Files
XCEngine/editor/src/ComponentEditors/ScriptComponentEditor.h

516 lines
22 KiB
C++

#pragma once
#include "Application.h"
#include "IComponentEditor.h"
#include "ScriptComponentEditorUtils.h"
#include "Core/IUndoManager.h"
#include "UI/UI.h"
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
namespace XCEngine {
namespace Editor {
class ScriptComponentEditor : public IComponentEditor {
public:
const char* GetComponentTypeName() const override {
return "ScriptComponent";
}
const char* GetDisplayName() const override {
return "Script";
}
bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override {
auto* scriptComponent = dynamic_cast<::XCEngine::Scripting::ScriptComponent*>(component);
if (!scriptComponent) {
return false;
}
bool changed = false;
changed |= RenderScriptClassSelector(*scriptComponent, undoManager);
::XCEngine::Scripting::ScriptFieldModel model;
if (!::XCEngine::Scripting::ScriptEngine::Get().TryGetScriptFieldModel(scriptComponent, model)) {
UI::DrawHintText("Failed to query script field metadata.");
return changed;
}
switch (model.classStatus) {
case ::XCEngine::Scripting::ScriptFieldClassStatus::Unassigned:
UI::DrawHintText("Assign a C# script to edit serialized fields.");
return changed;
case ::XCEngine::Scripting::ScriptFieldClassStatus::Missing:
UI::DrawHintText("Assigned script class is not available in the loaded script assemblies.");
break;
case ::XCEngine::Scripting::ScriptFieldClassStatus::Available:
default:
break;
}
if (model.fields.empty()) {
UI::DrawHintText(
model.classStatus == ::XCEngine::Scripting::ScriptFieldClassStatus::Available
? "Selected script exposes no supported public instance fields."
: "No serialized script fields are currently available.");
return changed;
}
for (const ::XCEngine::Scripting::ScriptFieldSnapshot& field : model.fields) {
changed |= RenderScriptField(*scriptComponent, model.classStatus, field, undoManager);
}
return changed;
}
bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override {
return gameObject != nullptr;
}
const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override {
return gameObject ? nullptr : "Invalid";
}
bool CanRemove(::XCEngine::Components::Component* component) const override {
return CanEdit(component);
}
private:
static constexpr size_t kStringBufferSize = 512;
struct StringFieldEditState {
std::array<char, kStringBufferSize> buffer{};
std::string lastSyncedValue;
bool initialized = false;
bool editing = false;
};
bool RenderScriptClassSelector(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
IUndoManager* undoManager) {
std::vector<::XCEngine::Scripting::ScriptClassDescriptor> scriptClasses;
const bool hasLoadedClasses =
::XCEngine::Scripting::ScriptEngine::Get().TryGetAvailableScriptClasses(scriptClasses);
const ::XCEngine::Scripting::ScriptClassDescriptor currentDescriptor{
scriptComponent.GetAssemblyName(),
scriptComponent.GetNamespaceName(),
scriptComponent.GetClassName()
};
std::string currentLabel = "None";
if (scriptComponent.HasScriptClass()) {
const auto currentIt = std::find(scriptClasses.begin(), scriptClasses.end(), currentDescriptor);
currentLabel = currentIt != scriptClasses.end()
? ScriptComponentEditorUtils::BuildScriptClassDisplayName(*currentIt)
: ScriptComponentEditorUtils::BuildScriptClassDisplayName(scriptComponent) + " (Missing)";
}
bool changed = false;
UI::DrawPropertyRow("Script", UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::SetNextItemWidth(layout.controlWidth);
if (!ImGui::BeginCombo("##ScriptClass", currentLabel.c_str())) {
return false;
}
if (ImGui::Selectable("None", !scriptComponent.HasScriptClass())) {
changed |= ApplyScriptClassSelection(scriptComponent, nullptr, undoManager);
}
if (!scriptClasses.empty()) {
ImGui::Separator();
}
for (const ::XCEngine::Scripting::ScriptClassDescriptor& descriptor : scriptClasses) {
const bool selected = descriptor == currentDescriptor;
const std::string label =
ScriptComponentEditorUtils::BuildScriptClassDisplayName(descriptor);
if (ImGui::Selectable(label.c_str(), selected)) {
changed |= ApplyScriptClassSelection(scriptComponent, &descriptor, undoManager);
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
if (!hasLoadedClasses) {
ImGui::Separator();
ImGui::TextDisabled("No script assemblies are currently loaded.");
}
ImGui::EndCombo();
return false;
});
if (!hasLoadedClasses) {
const EditorScriptRuntimeStatus& runtimeStatus = Application::Get().GetScriptRuntimeStatus();
const std::string hintText =
ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(runtimeStatus);
UI::DrawHintText(hintText.c_str());
if (ScriptComponentEditorUtils::CanRebuildScriptAssemblies(runtimeStatus)) {
if (UI::InspectorActionButton("Rebuild Scripts", ImVec2(120.0f, 0.0f))) {
Application::Get().RebuildScriptingAssemblies();
}
}
if (ScriptComponentEditorUtils::CanReloadScriptRuntime(runtimeStatus)) {
if (ScriptComponentEditorUtils::CanRebuildScriptAssemblies(runtimeStatus)) {
ImGui::SameLine();
}
if (UI::InspectorActionButton("Reload Scripts", ImVec2(120.0f, 0.0f))) {
Application::Get().ReloadScriptingRuntime();
}
}
}
return changed;
}
bool ApplyScriptClassSelection(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptClassDescriptor* descriptor,
IUndoManager* undoManager) const {
if (!descriptor) {
if (!scriptComponent.HasScriptClass()) {
return false;
}
if (undoManager) {
undoManager->BeginInteractiveChange("Modify Script Component");
}
scriptComponent.ClearScriptClass();
return true;
}
if (scriptComponent.GetAssemblyName() == descriptor->assemblyName &&
scriptComponent.GetNamespaceName() == descriptor->namespaceName &&
scriptComponent.GetClassName() == descriptor->className) {
return false;
}
if (undoManager) {
undoManager->BeginInteractiveChange("Modify Script Component");
}
scriptComponent.SetScriptClass(
descriptor->assemblyName,
descriptor->namespaceName,
descriptor->className);
return true;
}
bool RenderScriptField(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
::XCEngine::Scripting::ScriptFieldClassStatus classStatus,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
IUndoManager* undoManager) {
bool changed = false;
const bool canEdit = ScriptComponentEditorUtils::CanEditScriptField(classStatus, field);
if (canEdit) {
changed |= RenderScriptFieldEditor(scriptComponent, field, undoManager);
} else {
RenderReadOnlyScriptField(field);
}
const std::string issueText = ScriptComponentEditorUtils::BuildScriptFieldIssueText(field);
if (!issueText.empty()) {
UI::DrawHintText(issueText.c_str());
}
if (ScriptComponentEditorUtils::CanClearScriptFieldOverride(field)) {
ImGui::PushID((field.metadata.name + "##Reset").c_str());
if (UI::InspectorActionButton("Reset", ImVec2(72.0f, 0.0f))) {
changed |= ClearScriptFieldOverride(scriptComponent, field, undoManager);
}
ImGui::PopID();
}
return changed;
}
bool RenderScriptFieldEditor(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
IUndoManager* undoManager) {
using namespace ::XCEngine::Scripting;
switch (field.metadata.type) {
case ScriptFieldType::Float: {
float value = std::get<float>(field.value);
const bool widgetChanged = UI::DrawPropertyFloat(field.metadata.name.c_str(), value, 0.1f);
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::Double: {
double value = std::get<double>(field.value);
const bool widgetChanged = UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::SetNextItemWidth(layout.controlWidth);
return ImGui::InputDouble("##Value", &value, 0.0, 0.0, "%.3f");
});
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::Bool: {
bool value = std::get<bool>(field.value);
const bool widgetChanged = UI::DrawPropertyBool(field.metadata.name.c_str(), value);
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::Int32: {
int value = std::get<int32_t>(field.value);
const bool widgetChanged = UI::DrawPropertyInt(field.metadata.name.c_str(), value, 1);
return widgetChanged &&
ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(static_cast<int32_t>(value)), undoManager);
}
case ScriptFieldType::UInt64: {
uint64_t value = std::get<uint64_t>(field.value);
const bool widgetChanged = UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::SetNextItemWidth(layout.controlWidth);
return ImGui::InputScalar("##Value", ImGuiDataType_U64, &value);
});
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::String:
return RenderStringScriptFieldEditor(scriptComponent, field, undoManager);
case ScriptFieldType::Vector2: {
::XCEngine::Math::Vector2 value = std::get<::XCEngine::Math::Vector2>(field.value);
const bool widgetChanged = UI::DrawPropertyVec2(field.metadata.name.c_str(), value, 0.0f, 0.1f);
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::Vector3: {
::XCEngine::Math::Vector3 value = std::get<::XCEngine::Math::Vector3>(field.value);
const bool widgetChanged = UI::DrawPropertyVec3Input(field.metadata.name.c_str(), value, 0.1f);
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::Vector4: {
::XCEngine::Math::Vector4 value = std::get<::XCEngine::Math::Vector4>(field.value);
const bool widgetChanged = UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::SetNextItemWidth(layout.controlWidth);
return ImGui::DragFloat4("##Value", &value.x, 0.1f);
});
return widgetChanged && ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(value), undoManager);
}
case ScriptFieldType::GameObject:
return RenderGameObjectScriptFieldEditor(scriptComponent, field, undoManager);
case ScriptFieldType::None:
default:
RenderReadOnlyScriptField(field);
return false;
}
}
bool RenderStringScriptFieldEditor(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
IUndoManager* undoManager) {
StringFieldEditState& editState =
m_stringFieldStates[scriptComponent.GetScriptComponentUUID()][field.metadata.name];
const std::string currentValue = std::get<std::string>(field.value);
if (!editState.initialized || (!editState.editing && editState.lastSyncedValue != currentValue)) {
SyncStringFieldEditState(editState, currentValue);
}
bool isEditing = false;
const bool widgetChanged = UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::SetNextItemWidth(layout.controlWidth);
const bool changed = ImGui::InputText("##Value", editState.buffer.data(), editState.buffer.size());
isEditing = ImGui::IsItemActive();
return changed;
});
editState.editing = isEditing;
if (!widgetChanged) {
return false;
}
const std::string editedValue(editState.buffer.data());
if (!ApplyScriptFieldWrite(
scriptComponent,
field,
::XCEngine::Scripting::ScriptFieldValue(editedValue),
undoManager)) {
SyncStringFieldEditState(editState, currentValue);
return false;
}
editState.lastSyncedValue = editedValue;
return true;
}
bool RenderGameObjectScriptFieldEditor(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
IUndoManager* undoManager) {
using namespace ::XCEngine::Scripting;
const GameObjectReference currentReference = std::get<GameObjectReference>(field.value);
const auto& sceneRoots = Application::Get().GetEditorContext().GetSceneManager().GetRootEntities();
const ::XCEngine::Components::GameObject::ID currentGameObjectId =
ScriptComponentEditorUtils::FindGameObjectIdByUuid(sceneRoots, currentReference.gameObjectUUID);
UI::ReferencePickerOptions pickerOptions;
pickerOptions.popupTitle = "Select GameObject";
pickerOptions.emptyHint = "None";
pickerOptions.searchHint = "Search";
pickerOptions.noSceneText = "No scene objects.";
pickerOptions.showAssetsTab = false;
pickerOptions.showSceneTab = true;
GameObjectReference pendingReference = currentReference;
const bool widgetChanged = UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
const UI::ReferencePickerInteraction interaction =
UI::DrawReferencePickerControl(
std::string(),
currentGameObjectId,
pickerOptions,
layout.controlWidth);
if (interaction.clearRequested) {
pendingReference = GameObjectReference{};
return true;
}
if (interaction.assignedSceneObjectId == ::XCEngine::Components::GameObject::INVALID_ID ||
interaction.assignedSceneObjectId == currentGameObjectId) {
return false;
}
::XCEngine::Components::GameObject* assignedGameObject =
Application::Get().GetEditorContext().GetSceneManager().GetEntity(interaction.assignedSceneObjectId);
if (!assignedGameObject) {
return false;
}
pendingReference = GameObjectReference{assignedGameObject->GetUUID()};
return true;
});
return widgetChanged &&
ApplyScriptFieldWrite(scriptComponent, field, ScriptFieldValue(pendingReference), undoManager);
}
void RenderReadOnlyScriptField(const ::XCEngine::Scripting::ScriptFieldSnapshot& field) const {
const std::string valueText = DescribeScriptFieldValue(field.metadata.type, field.value);
UI::DrawPropertyRow(
field.metadata.name.c_str(),
UI::InspectorPropertyLayout(),
[&](const UI::PropertyLayoutMetrics& layout) {
UI::AlignPropertyControlVertically(layout, ImGui::GetTextLineHeight());
ImGui::TextDisabled("%s", valueText.c_str());
return false;
});
}
bool ApplyScriptFieldWrite(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
const ::XCEngine::Scripting::ScriptFieldValue& value,
IUndoManager* undoManager) const {
std::vector<::XCEngine::Scripting::ScriptFieldWriteResult> results;
if (undoManager) {
undoManager->BeginInteractiveChange("Modify Script Field");
}
const bool applied = ::XCEngine::Scripting::ScriptEngine::Get().ApplyScriptFieldWrites(
&scriptComponent,
{ ::XCEngine::Scripting::ScriptFieldWriteRequest{field.metadata.name, field.metadata.type, value} },
results);
if (!applied || results.empty() ||
results.front().status != ::XCEngine::Scripting::ScriptFieldWriteStatus::Applied) {
if (undoManager && undoManager->HasPendingInteractiveChange()) {
undoManager->CancelInteractiveChange();
}
return false;
}
return true;
}
bool ClearScriptFieldOverride(
::XCEngine::Scripting::ScriptComponent& scriptComponent,
const ::XCEngine::Scripting::ScriptFieldSnapshot& field,
IUndoManager* undoManager) const {
std::vector<::XCEngine::Scripting::ScriptFieldClearResult> results;
if (undoManager) {
undoManager->BeginInteractiveChange("Clear Script Field Override");
}
const bool cleared = ::XCEngine::Scripting::ScriptEngine::Get().ClearScriptFieldOverrides(
&scriptComponent,
{ ::XCEngine::Scripting::ScriptFieldClearRequest{field.metadata.name} },
results);
if (!cleared || results.empty() ||
results.front().status != ::XCEngine::Scripting::ScriptFieldClearStatus::Applied) {
if (undoManager && undoManager->HasPendingInteractiveChange()) {
undoManager->CancelInteractiveChange();
}
return false;
}
return true;
}
static void SyncStringFieldEditState(StringFieldEditState& editState, const std::string& value) {
editState.buffer.fill('\0');
const size_t copyLength = (std::min)(value.size(), editState.buffer.size() - 1);
if (copyLength > 0) {
std::memcpy(editState.buffer.data(), value.data(), copyLength);
}
editState.buffer[copyLength] = '\0';
editState.lastSyncedValue = value;
editState.initialized = true;
}
static std::string DescribeScriptFieldValue(
::XCEngine::Scripting::ScriptFieldType type,
const ::XCEngine::Scripting::ScriptFieldValue& value) {
if (type == ::XCEngine::Scripting::ScriptFieldType::GameObject) {
const auto reference = std::get<::XCEngine::Scripting::GameObjectReference>(value);
if (reference.gameObjectUUID == 0) {
return "None";
}
return "GameObject (" + std::to_string(reference.gameObjectUUID) + ")";
}
return ::XCEngine::Scripting::SerializeScriptFieldValue(type, value);
}
std::unordered_map<uint64_t, std::unordered_map<std::string, StringFieldEditState>> m_stringFieldStates;
};
} // namespace Editor
} // namespace XCEngine