Refactor new editor scene runtime ownership

This commit is contained in:
2026-04-18 00:45:14 +08:00
parent 2fe1076218
commit b48760ca3d
12 changed files with 792 additions and 326 deletions

View File

@@ -285,7 +285,9 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP)
)
set(XCUI_EDITOR_APP_SUPPORT_SOURCES
app/Scene/EditorSceneRuntime.cpp
app/Internal/EmbeddedPngLoader.cpp
app/Scene/EditorSceneBridge.cpp
)
set(XCUI_EDITOR_APP_PLATFORM_SOURCES

View File

@@ -2,6 +2,7 @@
#include "State/EditorContext.h"
#include <XCEditor/App/EditorPanelIds.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
namespace XCEngine::UI::Editor::App::Internal {
@@ -121,6 +122,7 @@ void EditorShellRuntime::Update(
const Widgets::UIEditorDockHostLayout preUpdateDockLayout =
m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout;
m_hierarchyPanel.SetSceneRuntime(&context.GetSceneRuntime());
context.BindEditCommandRoutes(&m_hierarchyPanel, &m_projectPanel);
context.SyncSessionFromWorkspace(workspaceController);
UIEditorShellInteractionDefinition definition =
@@ -175,15 +177,16 @@ void EditorShellRuntime::Update(
m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame,
hostedContentEvents,
!m_shellFrame.result.workspaceInputSuppressed,
activePanelId == "hierarchy");
activePanelId == kHierarchyPanelId);
m_projectPanel.Update(
m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame,
hostedContentEvents,
!m_shellFrame.result.workspaceInputSuppressed,
activePanelId == "project");
activePanelId == kProjectPanelId);
m_traceEntries = SyncWorkspaceEvents(context, *this);
m_inspectorPanel.Update(
context.GetSession(),
context.GetSceneRuntime(),
m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame);
m_consolePanel.Update(
context.GetSession(),

View File

@@ -5,6 +5,8 @@
#include "Features/Project/ProjectPanel.h"
#include "Composition/EditorShellRuntime.h"
#include <XCEditor/App/EditorPanelIds.h>
#include <sstream>
#include <utility>
@@ -114,15 +116,20 @@ void ApplyHierarchySelection(
return;
}
if (event.itemId.empty()) {
context.ClearSelection();
const EditorSceneRuntime& sceneRuntime = context.GetSceneRuntime();
if (!sceneRuntime.HasSceneSelection()) {
if (context.GetSession().selection.kind == EditorSelectionKind::HierarchyNode) {
context.ClearSelection();
}
return;
}
EditorSelectionState selection = {};
selection.kind = EditorSelectionKind::HierarchyNode;
selection.itemId = event.itemId;
selection.displayName = event.label.empty() ? event.itemId : event.label;
selection.itemId = sceneRuntime.GetSelectedItemId();
selection.displayName = sceneRuntime.GetSelectedDisplayName().empty()
? selection.itemId
: sceneRuntime.GetSelectedDisplayName();
context.SetSelection(std::move(selection));
}
@@ -166,14 +173,14 @@ std::vector<WorkspaceTraceEntry> SyncWorkspaceEvents(
ApplyHierarchySelection(context, event);
const std::string message = DescribeHierarchyPanelEvent(event);
context.SetStatus("Hierarchy", message);
entries.push_back(WorkspaceTraceEntry{ "hierarchy", std::move(message) });
entries.push_back(WorkspaceTraceEntry{ std::string(kHierarchyPanelId), std::move(message) });
}
for (const ProjectPanel::Event& event : runtime.GetProjectPanelEvents()) {
ApplyProjectSelection(context, event);
const std::string message = DescribeProjectPanelEvent(event);
context.SetStatus("Project", message);
entries.push_back(WorkspaceTraceEntry{ "project", std::move(message) });
entries.push_back(WorkspaceTraceEntry{ std::string(kProjectPanelId), std::move(message) });
}
return entries;

View File

@@ -1,10 +1,16 @@
#include "HierarchyPanelInternal.h"
#include "Scene/EditorSceneRuntime.h"
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Fields/UIEditorTextField.h>
#include <utility>
namespace XCEngine::UI::Editor::App {
using namespace HierarchyPanelInternal;
namespace TreeDrag = TreeItemDragDrop;
namespace {
@@ -26,11 +32,23 @@ UIEditorHostCommandDispatchResult BuildDispatchResult(
return result;
}
bool HasValidBounds(const UIRect& bounds) {
return bounds.width > 0.0f && bounds.height > 0.0f;
}
} // namespace
void HierarchyPanel::Initialize() {
m_model = HierarchyModel::BuildDefault();
RebuildItems();
SyncModelFromScene();
}
void HierarchyPanel::SetSceneRuntime(EditorSceneRuntime* sceneRuntime) {
if (m_sceneRuntime == sceneRuntime) {
return;
}
m_sceneRuntime = sceneRuntime;
SyncModelFromScene();
}
void HierarchyPanel::SetBuiltInIcons(const BuiltInIcons* icons) {
@@ -42,6 +60,7 @@ void HierarchyPanel::ResetInteractionState() {
m_treeInteractionState = {};
m_treeFrame = {};
m_dragState = {};
ClearRenameState();
ResetTransientState();
}
@@ -58,39 +77,89 @@ const UIEditorPanelContentHostPanelState* HierarchyPanel::FindMountedHierarchyPa
void HierarchyPanel::ResetTransientState() {
m_frameEvents.clear();
m_dragState.requestPointerCapture = false;
m_dragState.requestPointerRelease = false;
TreeDrag::ResetTransientRequests(m_dragState);
}
void HierarchyPanel::SyncModelFromScene() {
if (m_sceneRuntime != nullptr) {
m_sceneRuntime->RefreshScene();
}
const HierarchyModel sceneModel =
HierarchyModel::BuildFromScene(
m_sceneRuntime != nullptr
? m_sceneRuntime->GetActiveScene()
: nullptr);
if (!m_model.HasSameTree(sceneModel) || m_treeItems.empty()) {
m_model = sceneModel;
if (m_sceneRuntime != nullptr && !m_model.Empty()) {
m_sceneRuntime->EnsureSceneSelection();
}
RebuildItems();
return;
}
if (m_sceneRuntime != nullptr && !m_model.Empty()) {
m_sceneRuntime->EnsureSceneSelection();
}
SyncTreeSelectionFromSceneRuntime();
}
void HierarchyPanel::RebuildItems() {
const auto icon = ResolveGameObjectIcon(m_icons);
const std::string previousSelection =
m_selection.HasSelection() ? m_selection.GetSelectedId() : std::string();
m_treeItems = m_model.BuildTreeItems(icon);
m_expansion.Expand("player");
m_expansion.Expand("environment");
m_expansion.Expand("props");
SyncTreeSelectionFromSceneRuntime();
}
if (!previousSelection.empty() && m_model.ContainsNode(previousSelection)) {
m_selection.SetSelection(previousSelection);
void HierarchyPanel::SyncTreeSelectionFromSceneRuntime() {
if (m_sceneRuntime == nullptr) {
m_treeSelection.ClearSelection();
return;
}
if (!m_treeItems.empty()) {
m_selection.SetSelection(m_treeItems.front().itemId);
const std::string selectedItemId = m_sceneRuntime->GetSelectedItemId();
if (!selectedItemId.empty() && m_model.ContainsNode(selectedItemId)) {
m_treeSelection.SetSelection(selectedItemId);
return;
}
m_selection.ClearSelection();
m_treeSelection.ClearSelection();
}
void HierarchyPanel::SyncSceneRuntimeSelectionFromTree() {
if (m_sceneRuntime == nullptr) {
return;
}
if (m_treeSelection.HasSelection()) {
if (!m_sceneRuntime->SetSelection(m_treeSelection.GetSelectedId())) {
m_sceneRuntime->RefreshScene();
}
} else {
m_sceneRuntime->ClearSelection();
}
if (!m_model.Empty()) {
m_sceneRuntime->EnsureSceneSelection();
}
SyncTreeSelectionFromSceneRuntime();
}
const HierarchyNode* HierarchyPanel::GetSelectedNode() const {
if (!m_selection.HasSelection()) {
if (m_sceneRuntime != nullptr) {
const std::string selectedItemId = m_sceneRuntime->GetSelectedItemId();
if (!selectedItemId.empty()) {
return m_model.FindNode(selectedItemId);
}
}
if (!m_treeSelection.HasSelection()) {
return nullptr;
}
return m_model.FindNode(m_selection.GetSelectedId());
return m_model.FindNode(m_treeSelection.GetSelectedId());
}
void HierarchyPanel::EmitSelectionEvent() {
@@ -127,6 +196,155 @@ void HierarchyPanel::EmitRenameRequestedEvent(std::string_view itemId) {
m_frameEvents.push_back(std::move(event));
}
void HierarchyPanel::ClearRenameState() {
m_renameState = {};
m_renameFrame = {};
m_pendingRenameItemId.clear();
}
void HierarchyPanel::QueueRenameSession(std::string_view itemId) {
if (itemId.empty() || !m_model.ContainsNode(itemId)) {
return;
}
if (m_renameState.active && m_renameState.itemId == itemId) {
return;
}
m_pendingRenameItemId = std::string(itemId);
}
bool HierarchyPanel::TryStartQueuedRenameSession(
const Widgets::UIEditorTreeViewLayout& layout) {
if (m_pendingRenameItemId.empty()) {
return false;
}
const HierarchyNode* node = m_model.FindNode(m_pendingRenameItemId);
if (node == nullptr) {
m_pendingRenameItemId.clear();
return false;
}
const UIRect bounds = BuildRenameBounds(m_pendingRenameItemId, layout);
if (!HasValidBounds(bounds)) {
return false;
}
const Widgets::UIEditorTextFieldMetrics textFieldMetrics =
BuildUIEditorInlineRenameTextFieldMetrics(
bounds,
BuildUIEditorPropertyGridTextFieldMetrics(
ResolveUIEditorPropertyGridMetrics(),
ResolveUIEditorTextFieldMetrics()));
UIEditorInlineRenameSessionRequest request = {};
request.beginSession = true;
request.itemId = m_pendingRenameItemId;
request.initialText = node->label;
request.bounds = bounds;
m_renameFrame = UpdateUIEditorInlineRenameSession(
m_renameState,
request,
{},
textFieldMetrics);
if (m_renameFrame.result.sessionStarted) {
m_pendingRenameItemId.clear();
return true;
}
return false;
}
void HierarchyPanel::UpdateRenameSession(
const std::vector<UIInputEvent>& inputEvents,
const Widgets::UIEditorTreeViewLayout& layout) {
if (!m_renameState.active) {
return;
}
if (!m_model.ContainsNode(m_renameState.itemId)) {
ClearRenameState();
return;
}
const UIRect bounds = BuildRenameBounds(m_renameState.itemId, layout);
if (!HasValidBounds(bounds)) {
ClearRenameState();
return;
}
const Widgets::UIEditorTextFieldMetrics textFieldMetrics =
BuildUIEditorInlineRenameTextFieldMetrics(
bounds,
BuildUIEditorPropertyGridTextFieldMetrics(
ResolveUIEditorPropertyGridMetrics(),
ResolveUIEditorTextFieldMetrics()));
UIEditorInlineRenameSessionRequest request = {};
request.itemId = m_renameState.itemId;
request.initialText = m_renameState.textFieldSpec.value;
request.bounds = bounds;
m_renameFrame = UpdateUIEditorInlineRenameSession(
m_renameState,
request,
inputEvents,
textFieldMetrics);
if (!m_renameFrame.result.sessionCommitted) {
return;
}
if (m_renameFrame.result.valueChanged &&
m_sceneRuntime != nullptr) {
m_sceneRuntime->RenameGameObject(
m_renameFrame.result.itemId,
m_renameFrame.result.valueAfter);
}
SyncModelFromScene();
EmitSelectionEvent();
}
void HierarchyPanel::SyncTreeFocusState(
const std::vector<UIInputEvent>& inputEvents) {
for (const UIInputEvent& event : inputEvents) {
if (event.type == ::XCEngine::UI::UIInputEventType::FocusGained) {
m_treeInteractionState.treeViewState.focused = true;
} else if (event.type == ::XCEngine::UI::UIInputEventType::FocusLost) {
m_treeInteractionState.treeViewState.focused = false;
}
}
}
UIRect HierarchyPanel::BuildRenameBounds(
std::string_view itemId,
const Widgets::UIEditorTreeViewLayout& layout) const {
if (itemId.empty()) {
return {};
}
const std::size_t visibleIndex =
FindVisibleIndexForItemId(layout, m_treeItems, itemId);
if (visibleIndex == UIEditorTreeViewInvalidIndex ||
visibleIndex >= layout.rowRects.size() ||
visibleIndex >= layout.labelRects.size()) {
return {};
}
const Widgets::UIEditorTextFieldMetrics hostedMetrics =
BuildUIEditorPropertyGridTextFieldMetrics(
ResolveUIEditorPropertyGridMetrics(),
ResolveUIEditorTextFieldMetrics());
const UIRect& rowRect = layout.rowRects[visibleIndex];
const UIRect& labelRect = layout.labelRects[visibleIndex];
const float x = (std::max)(rowRect.x, labelRect.x - hostedMetrics.valueTextInsetX);
const float right = rowRect.x + rowRect.width - 8.0f;
const float width = (std::max)(120.0f, right - x);
return UIRect(x, rowRect.y, width, rowRect.height);
}
bool HierarchyPanel::WantsHostPointerCapture() const {
return m_dragState.requestPointerCapture;
}
@@ -136,7 +354,7 @@ bool HierarchyPanel::WantsHostPointerRelease() const {
}
bool HierarchyPanel::HasActivePointerCapture() const {
return m_dragState.dragging;
return TreeDrag::HasActivePointerCapture(m_dragState);
}
const std::vector<HierarchyPanel::Event>& HierarchyPanel::GetFrameEvents() const {
@@ -193,20 +411,27 @@ UIEditorHostCommandDispatchResult HierarchyPanel::DispatchEditCommand(
const std::string selectedNodeId = selectedNode->nodeId;
const std::string selectedNodeLabel = selectedNode->label;
if (m_sceneRuntime == nullptr) {
return BuildDispatchResult(false, "Hierarchy scene runtime is unavailable.");
}
if (commandId == "edit.rename") {
QueueRenameSession(selectedNodeId);
EmitRenameRequestedEvent(selectedNodeId);
if (m_visible) {
TryStartQueuedRenameSession(m_treeFrame.layout);
}
return BuildDispatchResult(
true,
"Hierarchy rename requested for '" + selectedNodeLabel + "'.");
}
if (commandId == "edit.delete") {
if (!m_model.DeleteNode(selectedNodeId)) {
if (!m_sceneRuntime->DeleteGameObject(selectedNodeId)) {
return BuildDispatchResult(false, "Failed to delete the selected hierarchy object.");
}
RebuildItems();
SyncModelFromScene();
EmitSelectionEvent();
return BuildDispatchResult(
true,
@@ -214,13 +439,13 @@ UIEditorHostCommandDispatchResult HierarchyPanel::DispatchEditCommand(
}
if (commandId == "edit.duplicate") {
const std::string duplicatedNodeId = m_model.DuplicateNode(selectedNodeId);
const std::string duplicatedNodeId =
m_sceneRuntime->DuplicateGameObject(selectedNodeId);
if (duplicatedNodeId.empty()) {
return BuildDispatchResult(false, "Failed to duplicate the selected hierarchy object.");
}
RebuildItems();
m_selection.SetSelection(duplicatedNodeId);
SyncModelFromScene();
EmitSelectionEvent();
const HierarchyNode* duplicatedNode = m_model.FindNode(duplicatedNodeId);
@@ -234,115 +459,6 @@ UIEditorHostCommandDispatchResult HierarchyPanel::DispatchEditCommand(
return BuildDispatchResult(false, "Hierarchy does not expose this edit command.");
}
std::vector<UIInputEvent> HierarchyPanel::BuildInteractionInputEvents(
const std::vector<UIInputEvent>& inputEvents,
const UIRect& bounds,
bool allowInteraction,
bool panelActive) const {
const std::vector<UIInputEvent> rawEvents = FilterHierarchyInputEvents(
bounds,
inputEvents,
allowInteraction,
panelActive,
HasActivePointerCapture());
struct DragPreviewState {
std::string armedItemId = {};
UIPoint pressPosition = {};
bool armed = false;
bool dragging = false;
};
const Widgets::UIEditorTreeViewLayout layout =
m_treeFrame.layout.bounds.width > 0.0f
? m_treeFrame.layout
: Widgets::BuildUIEditorTreeViewLayout(
bounds,
m_treeItems,
m_expansion,
ResolveUIEditorTreeViewMetrics());
DragPreviewState preview = {};
preview.armed = m_dragState.armed;
preview.armedItemId = m_dragState.armedItemId;
preview.pressPosition = m_dragState.pressPosition;
preview.dragging = m_dragState.dragging;
std::vector<UIInputEvent> filteredEvents = {};
filteredEvents.reserve(rawEvents.size());
for (const UIInputEvent& event : rawEvents) {
bool suppress = false;
switch (event.type) {
case UIInputEventType::PointerButtonDown:
if (event.pointerButton == UIPointerButton::Left) {
UIEditorTreeViewHitTarget hitTarget = {};
const Widgets::UIEditorTreeViewItem* hitItem =
ResolveHitItem(layout, m_treeItems, event.position, &hitTarget);
if (hitItem != nullptr && hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) {
preview.armed = true;
preview.armedItemId = hitItem->itemId;
preview.pressPosition = event.position;
} else {
preview.armed = false;
preview.armedItemId.clear();
}
}
if (preview.dragging) {
suppress = true;
}
break;
case UIInputEventType::PointerMove:
if (preview.dragging) {
suppress = true;
break;
}
if (preview.armed &&
ComputeSquaredDistance(event.position, preview.pressPosition) >=
kDragThreshold * kDragThreshold) {
preview.dragging = true;
suppress = true;
}
break;
case UIInputEventType::PointerButtonUp:
if (event.pointerButton == UIPointerButton::Left) {
if (preview.dragging) {
suppress = true;
preview.dragging = false;
}
preview.armed = false;
preview.armedItemId.clear();
} else if (preview.dragging) {
suppress = true;
}
break;
case UIInputEventType::PointerLeave:
if (preview.dragging) {
suppress = true;
}
break;
case UIInputEventType::FocusLost:
preview.armed = false;
preview.dragging = false;
preview.armedItemId.clear();
break;
default:
break;
}
if (!suppress) {
filteredEvents.push_back(event);
}
}
return filteredEvents;
}
void HierarchyPanel::ProcessDragAndFrameEvents(
const std::vector<UIInputEvent>& inputEvents,
const UIRect& bounds,
@@ -356,125 +472,70 @@ void HierarchyPanel::ProcessDragAndFrameEvents(
HasActivePointerCapture());
if (m_treeFrame.result.selectionChanged) {
SyncSceneRuntimeSelectionFromTree();
EmitSelectionEvent();
}
if (m_treeFrame.result.renameRequested &&
!m_treeFrame.result.renameItemId.empty()) {
Event event = {};
event.kind = EventKind::RenameRequested;
event.itemId = m_treeFrame.result.renameItemId;
if (const HierarchyNode* node = m_model.FindNode(event.itemId); node != nullptr) {
event.label = node->label;
struct HierarchyTreeDragCallbacks {
::XCEngine::UI::Widgets::UISelectionModel& selection;
HierarchyModel& model;
EditorSceneRuntime* sceneRuntime = nullptr;
bool IsItemSelected(std::string_view itemId) const {
return selection.IsSelected(itemId);
}
m_frameEvents.push_back(std::move(event));
bool SelectDraggedItem(std::string_view itemId) {
return selection.SetSelection(std::string(itemId));
}
bool CanDropOnItem(
std::string_view draggedItemId,
std::string_view targetItemId) const {
return model.CanReparent(draggedItemId, targetItemId);
}
bool CanDropToRoot(std::string_view draggedItemId) const {
return model.GetParentId(draggedItemId).has_value();
}
bool CommitDropOnItem(
std::string_view draggedItemId,
std::string_view targetItemId) {
return sceneRuntime != nullptr &&
sceneRuntime->ReparentGameObject(draggedItemId, targetItemId);
}
bool CommitDropToRoot(std::string_view draggedItemId) {
return sceneRuntime != nullptr &&
sceneRuntime->MoveGameObjectToRoot(draggedItemId);
}
} callbacks{ m_treeSelection, m_model, m_sceneRuntime };
const TreeDrag::ProcessResult dragResult =
TreeDrag::ProcessInputEvents(
m_dragState,
m_treeFrame.layout,
m_treeItems,
filteredEvents,
bounds,
callbacks,
kDragThreshold);
if (dragResult.selectionForced) {
SyncSceneRuntimeSelectionFromTree();
EmitSelectionEvent();
}
for (const UIInputEvent& event : filteredEvents) {
switch (event.type) {
case UIInputEventType::PointerButtonDown:
if (event.pointerButton == UIPointerButton::Left) {
UIEditorTreeViewHitTarget hitTarget = {};
const Widgets::UIEditorTreeViewItem* hitItem =
ResolveHitItem(m_treeFrame.layout, m_treeItems, event.position, &hitTarget);
if (hitItem != nullptr && hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) {
m_dragState.armed = true;
m_dragState.armedItemId = hitItem->itemId;
m_dragState.pressPosition = event.position;
} else {
m_dragState.armed = false;
m_dragState.armedItemId.clear();
}
}
break;
case UIInputEventType::PointerMove:
if (m_dragState.armed &&
!m_dragState.dragging &&
ComputeSquaredDistance(event.position, m_dragState.pressPosition) >=
kDragThreshold * kDragThreshold) {
m_dragState.dragging = !m_dragState.armedItemId.empty();
m_dragState.draggedItemId = m_dragState.armedItemId;
m_dragState.dropTargetItemId.clear();
m_dragState.dropToRoot = false;
m_dragState.validDropTarget = false;
if (m_dragState.dragging) {
m_dragState.requestPointerCapture = true;
if (!m_selection.IsSelected(m_dragState.draggedItemId)) {
m_selection.SetSelection(m_dragState.draggedItemId);
EmitSelectionEvent();
}
}
}
if (m_dragState.dragging) {
UIEditorTreeViewHitTarget hitTarget = {};
const Widgets::UIEditorTreeViewItem* hitItem =
ResolveHitItem(m_treeFrame.layout, m_treeItems, event.position, &hitTarget);
m_dragState.dropTargetItemId.clear();
m_dragState.dropToRoot = false;
m_dragState.validDropTarget = false;
if (hitItem != nullptr &&
(hitTarget.kind == UIEditorTreeViewHitTargetKind::Row ||
hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure)) {
m_dragState.dropTargetItemId = hitItem->itemId;
m_dragState.validDropTarget =
m_model.CanReparent(m_dragState.draggedItemId, m_dragState.dropTargetItemId);
} else if (ContainsPoint(bounds, event.position)) {
m_dragState.dropToRoot = true;
m_dragState.validDropTarget =
m_model.GetParentId(m_dragState.draggedItemId).has_value();
}
}
break;
case UIInputEventType::PointerButtonUp:
if (event.pointerButton != UIPointerButton::Left) {
break;
}
if (m_dragState.dragging) {
if (m_dragState.validDropTarget) {
const std::string draggedItemId = m_dragState.draggedItemId;
const std::string dropTargetItemId = m_dragState.dropTargetItemId;
const bool changed =
m_dragState.dropToRoot
? m_model.MoveToRoot(draggedItemId)
: m_model.Reparent(draggedItemId, dropTargetItemId);
if (changed) {
RebuildItems();
EmitReparentEvent(
m_dragState.dropToRoot ? EventKind::MovedToRoot : EventKind::Reparented,
draggedItemId,
dropTargetItemId);
}
}
m_dragState.armed = false;
m_dragState.dragging = false;
m_dragState.armedItemId.clear();
m_dragState.draggedItemId.clear();
m_dragState.dropTargetItemId.clear();
m_dragState.dropToRoot = false;
m_dragState.validDropTarget = false;
m_dragState.requestPointerRelease = true;
} else {
m_dragState.armed = false;
m_dragState.armedItemId.clear();
}
break;
case UIInputEventType::FocusLost:
if (m_dragState.dragging) {
m_dragState.requestPointerRelease = true;
}
m_dragState = {};
break;
default:
break;
}
if (dragResult.dropCommitted) {
SyncModelFromScene();
m_treeFrame.layout = Widgets::BuildUIEditorTreeViewLayout(
bounds,
m_treeItems,
m_expansion,
ResolveUIEditorTreeViewMetrics());
EmitReparentEvent(
dragResult.droppedToRoot ? EventKind::MovedToRoot : EventKind::Reparented,
dragResult.draggedItemId,
dragResult.dropTargetItemId);
}
}
@@ -491,28 +552,67 @@ void HierarchyPanel::Update(
m_visible = false;
m_treeFrame = {};
m_dragState = {};
ClearRenameState();
return;
}
if (m_treeItems.empty()) {
RebuildItems();
SyncModelFromScene();
if (m_renameState.active && !m_model.ContainsNode(m_renameState.itemId)) {
ClearRenameState();
} else if (!m_pendingRenameItemId.empty() &&
!m_model.ContainsNode(m_pendingRenameItemId)) {
m_pendingRenameItemId.clear();
}
m_visible = true;
const std::vector<UIInputEvent> interactionEvents =
BuildInteractionInputEvents(
inputEvents,
const std::vector<UIInputEvent> filteredEvents = FilterHierarchyInputEvents(
panelState->bounds,
inputEvents,
allowInteraction,
panelActive,
HasActivePointerCapture());
SyncTreeFocusState(filteredEvents);
const Widgets::UIEditorTreeViewMetrics treeMetrics =
ResolveUIEditorTreeViewMetrics();
const Widgets::UIEditorTreeViewLayout layout =
Widgets::BuildUIEditorTreeViewLayout(
panelState->bounds,
allowInteraction,
panelActive);
m_treeItems,
m_expansion,
treeMetrics);
if (m_renameState.active || !m_pendingRenameItemId.empty()) {
m_treeFrame.layout = layout;
m_treeFrame.result = {};
TryStartQueuedRenameSession(layout);
UpdateRenameSession(filteredEvents, layout);
return;
}
const std::vector<UIInputEvent> interactionEvents =
TreeDrag::BuildInteractionInputEvents(
m_dragState,
layout,
m_treeItems,
filteredEvents,
kDragThreshold);
m_treeFrame = UpdateUIEditorTreeViewInteraction(
m_treeInteractionState,
m_selection,
m_treeSelection,
m_expansion,
panelState->bounds,
m_treeItems,
interactionEvents,
ResolveUIEditorTreeViewMetrics());
treeMetrics);
if (m_treeFrame.result.renameRequested &&
!m_treeFrame.result.renameItemId.empty()) {
QueueRenameSession(m_treeFrame.result.renameItemId);
EmitRenameRequestedEvent(m_treeFrame.result.renameItemId);
TryStartQueuedRenameSession(m_treeFrame.layout);
return;
}
ProcessDragAndFrameEvents(
inputEvents,
panelState->bounds,
@@ -533,7 +633,7 @@ void HierarchyPanel::Append(UIDrawList& drawList) const {
drawList,
m_treeFrame.layout,
m_treeItems,
m_selection,
m_treeSelection,
m_treeInteractionState.treeViewState,
palette,
metrics);
@@ -544,6 +644,33 @@ void HierarchyPanel::Append(UIDrawList& drawList) const {
palette,
metrics);
if (m_renameState.active) {
const Widgets::UIEditorTextFieldPalette textFieldPalette =
BuildUIEditorPropertyGridTextFieldPalette(
ResolveUIEditorPropertyGridPalette(),
ResolveUIEditorTextFieldPalette());
const Widgets::UIEditorTextFieldMetrics textFieldMetrics =
BuildUIEditorInlineRenameTextFieldMetrics(
BuildRenameBounds(m_renameState.itemId, m_treeFrame.layout),
BuildUIEditorPropertyGridTextFieldMetrics(
ResolveUIEditorPropertyGridMetrics(),
ResolveUIEditorTextFieldMetrics()));
Widgets::AppendUIEditorTextFieldBackground(
drawList,
m_renameFrame.layout,
m_renameState.textFieldSpec,
m_renameState.textFieldInteraction.textFieldState,
textFieldPalette,
textFieldMetrics);
Widgets::AppendUIEditorTextFieldForeground(
drawList,
m_renameFrame.layout,
m_renameState.textFieldSpec,
m_renameState.textFieldInteraction.textFieldState,
textFieldPalette,
textFieldMetrics);
}
if (!m_dragState.dragging || !m_dragState.validDropTarget) {
return;
}
@@ -557,7 +684,7 @@ void HierarchyPanel::Append(UIDrawList& drawList) const {
return;
}
const std::size_t visibleIndex = FindVisibleIndexForItemId(
const std::size_t visibleIndex = TreeDrag::FindVisibleIndexForItemId(
m_treeFrame.layout,
m_treeItems,
m_dragState.dropTargetItemId);

View File

@@ -1,8 +1,10 @@
#pragma once
#include "Composition/EditorEditCommandRoute.h"
#include "Features/Shared/TreeItemDragDrop.h"
#include "HierarchyModel.h"
#include <XCEditor/App/EditorEditCommandRoute.h>
#include <XCEditor/Collections/UIEditorInlineRenameSession.h>
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
#include <XCEditor/Panels/UIEditorPanelContentHost.h>
@@ -17,6 +19,7 @@
namespace XCEngine::UI::Editor::App {
class BuiltInIcons;
class EditorSceneRuntime;
class HierarchyPanel final : public EditorEditCommandRoute {
public:
@@ -36,6 +39,7 @@ public:
};
void Initialize();
void SetSceneRuntime(EditorSceneRuntime* sceneRuntime);
void SetBuiltInIcons(const BuiltInIcons* icons);
void ResetInteractionState();
void Update(
@@ -54,22 +58,10 @@ public:
std::string_view commandId) override;
private:
struct DragState {
std::string armedItemId = {};
std::string draggedItemId = {};
std::string dropTargetItemId = {};
::XCEngine::UI::UIPoint pressPosition = {};
bool armed = false;
bool dragging = false;
bool dropToRoot = false;
bool validDropTarget = false;
bool requestPointerCapture = false;
bool requestPointerRelease = false;
};
const UIEditorPanelContentHostPanelState* FindMountedHierarchyPanel(
const UIEditorPanelContentHostFrame& contentHostFrame) const;
void ResetTransientState();
void SyncModelFromScene();
void RebuildItems();
void ProcessDragAndFrameEvents(
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
@@ -77,27 +69,40 @@ private:
bool allowInteraction,
bool panelActive);
const HierarchyNode* GetSelectedNode() const;
void SyncTreeSelectionFromSceneRuntime();
void SyncSceneRuntimeSelectionFromTree();
void EmitSelectionEvent();
void EmitReparentEvent(
EventKind kind,
std::string itemId,
std::string targetItemId);
void EmitRenameRequestedEvent(std::string_view itemId);
std::vector<::XCEngine::UI::UIInputEvent> BuildInteractionInputEvents(
void ClearRenameState();
void QueueRenameSession(std::string_view itemId);
bool TryStartQueuedRenameSession(
const Widgets::UIEditorTreeViewLayout& layout);
void UpdateRenameSession(
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
const ::XCEngine::UI::UIRect& bounds,
bool allowInteraction,
bool panelActive) const;
const Widgets::UIEditorTreeViewLayout& layout);
void SyncTreeFocusState(
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents);
::XCEngine::UI::UIRect BuildRenameBounds(
std::string_view itemId,
const Widgets::UIEditorTreeViewLayout& layout) const;
const BuiltInIcons* m_icons = nullptr;
EditorSceneRuntime* m_sceneRuntime = nullptr;
HierarchyModel m_model = {};
std::vector<Widgets::UIEditorTreeViewItem> m_treeItems = {};
::XCEngine::UI::Widgets::UISelectionModel m_selection = {};
::XCEngine::UI::Widgets::UISelectionModel m_treeSelection = {};
::XCEngine::UI::Widgets::UIExpansionModel m_expansion = {};
UIEditorTreeViewInteractionState m_treeInteractionState = {};
UIEditorTreeViewInteractionFrame m_treeFrame = {};
UIEditorInlineRenameSessionState m_renameState = {};
UIEditorInlineRenameSessionFrame m_renameFrame = {};
std::string m_pendingRenameItemId = {};
std::vector<Event> m_frameEvents = {};
DragState m_dragState = {};
TreeItemDragDrop::State m_dragState = {};
bool m_visible = false;
};

View File

@@ -1,5 +1,8 @@
#include "InspectorPanel.h"
#include "Scene/EditorSceneRuntime.h"
#include <XCEditor/App/EditorPanelIds.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <algorithm>
@@ -13,7 +16,6 @@ using ::XCEngine::UI::UIDrawList;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIRect;
constexpr std::string_view kInspectorPanelId = "inspector";
constexpr float kPanelPadding = 10.0f;
constexpr float kHeaderHeight = 22.0f;
constexpr float kSectionGap = 10.0f;
@@ -55,32 +57,15 @@ const UIEditorPanelContentHostPanelState* InspectorPanel::FindMountedInspectorPa
return nullptr;
}
void InspectorPanel::BuildPresentation(const EditorSession& session) {
void InspectorPanel::BuildPresentation(
const EditorSession& session,
const EditorSceneRuntime& sceneRuntime) {
m_sections.clear();
m_title.clear();
m_subtitle.clear();
m_hasSelection = false;
switch (session.selection.kind) {
case EditorSelectionKind::HierarchyNode: {
m_hasSelection = true;
m_title = session.selection.displayName.empty()
? std::string("GameObject")
: session.selection.displayName;
m_subtitle = "GameObject";
Section identity = {};
identity.title = "Identity";
identity.rows = {
{ "Type", "GameObject" },
{ "Name", m_title },
{ "Id", session.selection.itemId }
};
m_sections.push_back(std::move(identity));
break;
}
case EditorSelectionKind::ProjectItem: {
if (session.selection.kind == EditorSelectionKind::ProjectItem) {
m_hasSelection = true;
m_title = session.selection.displayName.empty()
? (session.selection.directory ? std::string("Folder") : std::string("Asset"))
@@ -102,19 +87,50 @@ void InspectorPanel::BuildPresentation(const EditorSession& session) {
{ "Path", PathToUtf8String(session.selection.absolutePath) }
};
m_sections.push_back(std::move(location));
break;
return;
}
case EditorSelectionKind::None:
default:
m_title = "Nothing selected";
m_subtitle = "Select a hierarchy item or project asset.";
break;
if (session.selection.kind == EditorSelectionKind::HierarchyNode &&
sceneRuntime.HasSceneSelection()) {
const auto* selectedGameObject = sceneRuntime.GetSelectedGameObject();
m_hasSelection = true;
m_title = sceneRuntime.GetSelectedDisplayName().empty()
? std::string("GameObject")
: sceneRuntime.GetSelectedDisplayName();
m_subtitle = "GameObject";
Section identity = {};
identity.title = "Identity";
identity.rows = {
{ "Type", "GameObject" },
{ "Name", m_title },
{ "Id", sceneRuntime.GetSelectedItemId() }
};
m_sections.push_back(std::move(identity));
if (selectedGameObject != nullptr) {
Section hierarchy = {};
hierarchy.title = "Hierarchy";
hierarchy.rows = {
{ "Children", std::to_string(selectedGameObject->GetChildCount()) },
{ "Parent", selectedGameObject->GetParent() != nullptr
? (selectedGameObject->GetParent()->GetName().empty()
? std::string("GameObject")
: selectedGameObject->GetParent()->GetName())
: std::string("Scene Root") }
};
m_sections.push_back(std::move(hierarchy));
}
return;
}
m_title = "Nothing selected";
m_subtitle = "Select a hierarchy item or project asset.";
}
void InspectorPanel::Update(
const EditorSession& session,
const EditorSceneRuntime& sceneRuntime,
const UIEditorPanelContentHostFrame& contentHostFrame) {
const UIEditorPanelContentHostPanelState* panelState =
FindMountedInspectorPanel(contentHostFrame);
@@ -130,7 +146,7 @@ void InspectorPanel::Update(
m_visible = true;
m_bounds = panelState->bounds;
BuildPresentation(session);
BuildPresentation(session, sceneRuntime);
}
void InspectorPanel::Append(UIDrawList& drawList) const {

View File

@@ -1,6 +1,6 @@
#pragma once
#include "State/EditorSession.h"
#include <XCEditor/App/EditorSession.h>
#include <XCEditor/Panels/UIEditorPanelContentHost.h>
@@ -11,10 +11,13 @@
namespace XCEngine::UI::Editor::App {
class EditorSceneRuntime;
class InspectorPanel {
public:
void Update(
const EditorSession& session,
const EditorSceneRuntime& sceneRuntime,
const UIEditorPanelContentHostFrame& contentHostFrame);
void Append(::XCEngine::UI::UIDrawList& drawList) const;
@@ -31,7 +34,9 @@ private:
const UIEditorPanelContentHostPanelState* FindMountedInspectorPanel(
const UIEditorPanelContentHostFrame& contentHostFrame) const;
void BuildPresentation(const EditorSession& session);
void BuildPresentation(
const EditorSession& session,
const EditorSceneRuntime& sceneRuntime);
bool m_visible = false;
bool m_hasSelection = false;

View File

@@ -0,0 +1,183 @@
#include "Scene/EditorSceneRuntime.h"
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Scene/Scene.h>
namespace XCEngine::UI::Editor::App {
namespace {
using ::XCEngine::Components::GameObject;
using ::XCEngine::Components::Scene;
std::string ResolveGameObjectDisplayName(const GameObject& gameObject) {
return gameObject.GetName().empty()
? std::string("GameObject")
: gameObject.GetName();
}
} // namespace
bool EditorSceneRuntime::Initialize(const std::filesystem::path& projectRoot) {
m_projectRoot = projectRoot;
m_startupSceneResult = EnsureEditorStartupScene(projectRoot);
RefreshScene();
return m_startupSceneResult.ready;
}
void EditorSceneRuntime::RefreshScene() {
if (!HasValidSelection()) {
m_selectedGameObjectId.reset();
}
}
void EditorSceneRuntime::EnsureSceneSelection() {
if (HasValidSelection()) {
return;
}
SelectFirstAvailableGameObject();
}
const EditorStartupSceneResult& EditorSceneRuntime::GetStartupResult() const {
return m_startupSceneResult;
}
Scene* EditorSceneRuntime::GetActiveScene() const {
return GetActiveEditorScene();
}
bool EditorSceneRuntime::HasSceneSelection() const {
return HasValidSelection();
}
std::optional<GameObject::ID> EditorSceneRuntime::GetSelectedGameObjectId() const {
return HasValidSelection() ? m_selectedGameObjectId : std::nullopt;
}
std::string EditorSceneRuntime::GetSelectedItemId() const {
const std::optional<GameObject::ID> selectedId = GetSelectedGameObjectId();
return selectedId.has_value()
? MakeEditorGameObjectItemId(selectedId.value())
: std::string();
}
std::string EditorSceneRuntime::GetSelectedDisplayName() const {
const GameObject* gameObject = GetSelectedGameObject();
return gameObject != nullptr
? ResolveGameObjectDisplayName(*gameObject)
: std::string();
}
const GameObject* EditorSceneRuntime::GetSelectedGameObject() const {
if (!m_selectedGameObjectId.has_value()) {
return nullptr;
}
Scene* scene = GetActiveScene();
return scene != nullptr
? scene->FindByID(m_selectedGameObjectId.value())
: nullptr;
}
bool EditorSceneRuntime::SetSelection(std::string_view itemId) {
const std::optional<GameObject::ID> gameObjectId =
ParseEditorGameObjectItemId(itemId);
if (!gameObjectId.has_value()) {
return false;
}
return SetSelection(gameObjectId.value());
}
bool EditorSceneRuntime::SetSelection(GameObject::ID id) {
if (id == GameObject::INVALID_ID) {
return false;
}
Scene* scene = GetActiveScene();
if (scene == nullptr || scene->FindByID(id) == nullptr) {
return false;
}
const bool changed =
!m_selectedGameObjectId.has_value() ||
m_selectedGameObjectId.value() != id;
m_selectedGameObjectId = id;
return changed;
}
void EditorSceneRuntime::ClearSelection() {
m_selectedGameObjectId.reset();
}
GameObject* EditorSceneRuntime::FindGameObject(std::string_view itemId) const {
return FindEditorGameObject(itemId);
}
bool EditorSceneRuntime::RenameGameObject(
std::string_view itemId,
std::string_view newName) {
const bool renamed = RenameEditorGameObject(itemId, newName);
RefreshScene();
return renamed;
}
bool EditorSceneRuntime::DeleteGameObject(std::string_view itemId) {
const bool deleted = DeleteEditorGameObject(itemId);
RefreshScene();
EnsureSceneSelection();
return deleted;
}
std::string EditorSceneRuntime::DuplicateGameObject(std::string_view itemId) {
const std::string duplicatedItemId = DuplicateEditorGameObject(itemId);
if (!duplicatedItemId.empty()) {
SetSelection(duplicatedItemId);
} else {
RefreshScene();
}
return duplicatedItemId;
}
bool EditorSceneRuntime::ReparentGameObject(
std::string_view itemId,
std::string_view parentItemId) {
const bool reparented =
ReparentEditorGameObject(itemId, parentItemId);
RefreshScene();
return reparented;
}
bool EditorSceneRuntime::MoveGameObjectToRoot(std::string_view itemId) {
const bool moved = MoveEditorGameObjectToRoot(itemId);
RefreshScene();
return moved;
}
bool EditorSceneRuntime::HasValidSelection() const {
return GetSelectedGameObject() != nullptr;
}
bool EditorSceneRuntime::SelectFirstAvailableGameObject() {
Scene* scene = GetActiveScene();
if (scene == nullptr) {
m_selectedGameObjectId.reset();
return false;
}
for (GameObject* root : scene->GetRootGameObjects()) {
if (root == nullptr) {
continue;
}
m_selectedGameObjectId = root->GetID();
return true;
}
m_selectedGameObjectId.reset();
return false;
}
} // namespace XCEngine::UI::Editor::App

View File

@@ -0,0 +1,59 @@
#pragma once
#include "Scene/EditorSceneBridge.h"
#include <optional>
#include <string>
#include <string_view>
namespace XCEngine::Components {
class GameObject;
class Scene;
} // namespace XCEngine::Components
namespace XCEngine::UI::Editor::App {
class EditorSceneRuntime {
public:
bool Initialize(const std::filesystem::path& projectRoot);
void RefreshScene();
void EnsureSceneSelection();
const EditorStartupSceneResult& GetStartupResult() const;
::XCEngine::Components::Scene* GetActiveScene() const;
bool HasSceneSelection() const;
std::optional<::XCEngine::Components::GameObject::ID> GetSelectedGameObjectId() const;
std::string GetSelectedItemId() const;
std::string GetSelectedDisplayName() const;
const ::XCEngine::Components::GameObject* GetSelectedGameObject() const;
bool SetSelection(std::string_view itemId);
bool SetSelection(::XCEngine::Components::GameObject::ID id);
void ClearSelection();
::XCEngine::Components::GameObject* FindGameObject(std::string_view itemId) const;
bool RenameGameObject(
std::string_view itemId,
std::string_view newName);
bool DeleteGameObject(std::string_view itemId);
std::string DuplicateGameObject(std::string_view itemId);
bool ReparentGameObject(
std::string_view itemId,
std::string_view parentItemId);
bool MoveGameObjectToRoot(std::string_view itemId);
private:
bool HasValidSelection() const;
bool SelectFirstAvailableGameObject();
std::filesystem::path m_projectRoot = {};
EditorStartupSceneResult m_startupSceneResult = {};
std::optional<::XCEngine::Components::GameObject::ID> m_selectedGameObjectId = std::nullopt;
};
} // namespace XCEngine::UI::Editor::App

View File

@@ -1,6 +1,7 @@
#include "EditorContext.h"
#include "Composition/EditorShellAssetBuilder.h"
#include "Scene/EditorSceneRuntime.h"
#include <sstream>
#include <utility>
@@ -28,7 +29,7 @@ std::string ComposeStatusText(
} // namespace
bool EditorContext::Initialize(const std::filesystem::path& repoRoot) {
m_shellAsset = BuildEditorShellAsset(repoRoot);
m_shellAsset = BuildEditorApplicationShellAsset(repoRoot);
m_shellValidation = ValidateEditorShellAsset(m_shellAsset);
if (!m_shellValidation.IsValid()) {
return false;
@@ -37,6 +38,8 @@ bool EditorContext::Initialize(const std::filesystem::path& repoRoot) {
m_session = {};
m_session.repoRoot = repoRoot;
m_session.projectRoot = (repoRoot / "project").lexically_normal();
m_sceneRuntime = {};
m_sceneRuntime.Initialize(m_session.projectRoot);
m_hostCommandBridge.BindSession(m_session);
m_shortcutManager = BuildEditorShellShortcutManager(m_shellAsset);
m_shortcutManager.SetHostCommandHandler(&m_hostCommandBridge);
@@ -83,6 +86,14 @@ const EditorSession& EditorContext::GetSession() const {
return m_session;
}
EditorSceneRuntime& EditorContext::GetSceneRuntime() {
return m_sceneRuntime;
}
const EditorSceneRuntime& EditorContext::GetSceneRuntime() const {
return m_sceneRuntime;
}
void EditorContext::SetSelection(EditorSelectionState selection) {
m_session.selection = std::move(selection);
}
@@ -106,7 +117,7 @@ UIEditorShellInteractionDefinition EditorContext::BuildShellDefinition(
const UIEditorWorkspaceController& workspaceController,
std::string_view captureText,
EditorShellVariant variant) const {
return BuildEditorShellInteractionDefinition(
return BuildEditorApplicationShellInteractionDefinition(
m_shellAsset,
workspaceController,
ComposeStatusText(m_lastStatus, m_lastMessage),

View File

@@ -1,9 +1,10 @@
#pragma once
#include "Composition/EditorHostCommandBridge.h"
#include "State/EditorSession.h"
#include "Composition/EditorShellVariant.h"
#include "Scene/EditorSceneRuntime.h"
#include <XCEditor/App/EditorHostCommandBridge.h>
#include <XCEditor/App/EditorSession.h>
#include <XCEditor/Foundation/UIEditorShortcutManager.h>
#include <XCEditor/Shell/UIEditorShellAsset.h>
#include <XCEditor/Shell/UIEditorShellInteraction.h>
@@ -32,6 +33,8 @@ public:
const std::string& GetValidationMessage() const;
const EditorShellAsset& GetShellAsset() const;
const EditorSession& GetSession() const;
EditorSceneRuntime& GetSceneRuntime();
const EditorSceneRuntime& GetSceneRuntime() const;
void SetSelection(EditorSelectionState selection);
void ClearSelection();
@@ -60,6 +63,7 @@ private:
UIEditorShortcutManager m_shortcutManager = {};
UIEditorShellInteractionServices m_shellServices = {};
EditorSession m_session = {};
EditorSceneRuntime m_sceneRuntime = {};
EditorHostCommandBridge m_hostCommandBridge = {};
std::string m_lastStatus = {};
std::string m_lastMessage = {};

View File

@@ -68,11 +68,6 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
add_executable(editor_ui_tests ${EDITOR_UI_UNIT_TEST_SOURCES})
target_sources(editor_ui_tests
PRIVATE
${CMAKE_SOURCE_DIR}/new_editor/app/Composition/EditorHostCommandBridge.cpp
)
target_link_libraries(editor_ui_tests
PRIVATE
XCUIEditorLib
@@ -105,3 +100,52 @@ include(GoogleTest)
gtest_discover_tests(editor_ui_tests
DISCOVERY_MODE PRE_TEST
)
if(TARGET XCUIEditorAppLib)
add_executable(editor_app_feature_tests
test_project_browser_model.cpp
test_hierarchy_scene_binding.cpp
)
target_link_libraries(editor_app_feature_tests
PRIVATE
XCUIEditorAppLib
XCUIEditorLib
XCUIEditorHost
GTest::gtest_main
)
target_include_directories(editor_app_feature_tests
PRIVATE
${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/include
${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}
${CMAKE_SOURCE_DIR}/engine/include
)
if(MSVC)
target_compile_options(editor_app_feature_tests PRIVATE /utf-8 /FS)
set_target_properties(editor_app_feature_tests PROPERTIES
MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>"
COMPILE_PDB_NAME "editor_app_feature_tests-compile"
COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb"
COMPILE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Debug"
COMPILE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Release"
COMPILE_PDB_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/MinSizeRel"
COMPILE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/RelWithDebInfo"
)
set_property(TARGET editor_app_feature_tests PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
if(WIN32 AND EXISTS "${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll")
add_custom_command(TARGET editor_app_feature_tests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll
$<TARGET_FILE_DIR:editor_app_feature_tests>/assimp-vc143-mt.dll
)
endif()
gtest_discover_tests(editor_app_feature_tests
DISCOVERY_MODE PRE_TEST
)
endif()