From b7ce8618d2f027f199264f97b7c121ad616704bd Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 12 Apr 2026 11:12:27 +0800 Subject: [PATCH] Advance new editor hosted panels and state flow --- new_editor/CMakeLists.txt | 5 + new_editor/app/Application.cpp | 38 +- new_editor/app/Core/ProductEditorContext.cpp | 24 + new_editor/app/Core/ProductEditorContext.h | 4 + new_editor/app/Core/ProductEditorSession.cpp | 12 + new_editor/app/Core/ProductEditorSession.h | 22 + .../app/Hierarchy/ProductHierarchyModel.cpp | 293 ++++++++++++ .../app/Hierarchy/ProductHierarchyModel.h | 54 +++ new_editor/app/Panels/ProductConsolePanel.cpp | 109 +++++ new_editor/app/Panels/ProductConsolePanel.h | 27 ++ .../app/Panels/ProductHierarchyPanel.cpp | 439 ++++++++++++++++-- new_editor/app/Panels/ProductHierarchyPanel.h | 55 +++ .../app/Panels/ProductInspectorPanel.cpp | 224 +++++++++ new_editor/app/Panels/ProductInspectorPanel.h | 44 ++ new_editor/app/Panels/ProductProjectPanel.cpp | 377 +++------------ new_editor/app/Panels/ProductProjectPanel.h | 26 +- .../Project/ProductProjectBrowserModel.cpp | 376 +++++++++++++++ .../app/Project/ProductProjectBrowserModel.h | 67 +++ new_editor/app/Shell/ProductShellAsset.cpp | 8 +- .../app/Workspace/ProductEditorWorkspace.cpp | 30 +- .../app/Workspace/ProductEditorWorkspace.h | 8 + .../ProductEditorWorkspaceEventRouter.cpp | 178 +++++++ .../ProductEditorWorkspaceEventRouter.h | 20 + 23 files changed, 2059 insertions(+), 381 deletions(-) create mode 100644 new_editor/app/Hierarchy/ProductHierarchyModel.cpp create mode 100644 new_editor/app/Hierarchy/ProductHierarchyModel.h create mode 100644 new_editor/app/Panels/ProductConsolePanel.cpp create mode 100644 new_editor/app/Panels/ProductConsolePanel.h create mode 100644 new_editor/app/Panels/ProductInspectorPanel.cpp create mode 100644 new_editor/app/Panels/ProductInspectorPanel.h create mode 100644 new_editor/app/Project/ProductProjectBrowserModel.cpp create mode 100644 new_editor/app/Project/ProductProjectBrowserModel.h create mode 100644 new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.cpp create mode 100644 new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.h diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 083c3af1..f95f0203 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -150,11 +150,16 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) app/Commands/ProductEditorHostCommandBridge.cpp app/Core/ProductEditorContext.cpp app/Core/ProductEditorSession.cpp + app/Panels/ProductConsolePanel.cpp + app/Hierarchy/ProductHierarchyModel.cpp app/Icons/ProductBuiltInIcons.cpp app/Panels/ProductHierarchyPanel.cpp + app/Panels/ProductInspectorPanel.cpp app/Panels/ProductProjectPanel.cpp + app/Project/ProductProjectBrowserModel.cpp app/Shell/ProductShellAsset.cpp app/Workspace/ProductEditorWorkspace.cpp + app/Workspace/ProductEditorWorkspaceEventRouter.cpp ) target_include_directories(XCUIEditorApp PRIVATE diff --git a/new_editor/app/Application.cpp b/new_editor/app/Application.cpp index 06bb1af7..873f9756 100644 --- a/new_editor/app/Application.cpp +++ b/new_editor/app/Application.cpp @@ -344,6 +344,39 @@ std::string DescribeProjectPanelEvent(const App::ProductProjectPanel::Event& eve return stream.str(); } +std::string DescribeHierarchyPanelEvent(const App::ProductHierarchyPanel::Event& event) { + std::ostringstream stream = {}; + switch (event.kind) { + case App::ProductHierarchyPanel::EventKind::SelectionChanged: + stream << "SelectionChanged"; + break; + case App::ProductHierarchyPanel::EventKind::Reparented: + stream << "Reparented"; + break; + case App::ProductHierarchyPanel::EventKind::MovedToRoot: + stream << "MovedToRoot"; + break; + case App::ProductHierarchyPanel::EventKind::RenameRequested: + stream << "RenameRequested"; + break; + case App::ProductHierarchyPanel::EventKind::None: + default: + stream << "None"; + break; + } + + if (!event.itemId.empty()) { + stream << " item=" << event.itemId; + } + if (!event.targetItemId.empty()) { + stream << " target=" << event.targetItemId; + } + if (!event.label.empty()) { + stream << " label=" << event.label; + } + return stream.str(); +} + } // namespace int Application::Run(HINSTANCE hInstance, int nCmdShow) { @@ -534,9 +567,8 @@ void Application::RenderFrame() { frameTrace.str()); } ApplyHostCaptureRequests(shellFrame.result); - for (const App::ProductProjectPanel::Event& event : m_editorWorkspace.GetProjectPanelEvents()) { - LogRuntimeTrace("project", DescribeProjectPanelEvent(event)); - m_editorContext.SetStatus("Project", DescribeProjectPanelEvent(event)); + for (const App::ProductEditorWorkspaceTraceEntry& entry : m_editorWorkspace.GetTraceEntries()) { + LogRuntimeTrace(entry.channel, entry.message); } ApplyHostedContentCaptureRequests(); ApplyCurrentCursor(); diff --git a/new_editor/app/Core/ProductEditorContext.cpp b/new_editor/app/Core/ProductEditorContext.cpp index d136a110..74df0c59 100644 --- a/new_editor/app/Core/ProductEditorContext.cpp +++ b/new_editor/app/Core/ProductEditorContext.cpp @@ -11,6 +11,7 @@ namespace XCEngine::UI::Editor::App { namespace { using ::XCEngine::UI::Editor::BuildEditorShellShortcutManager; +constexpr std::size_t kMaxConsoleEntries = 256u; std::string ComposeStatusText( std::string_view status, @@ -82,6 +83,14 @@ const ProductEditorSession& ProductEditorContext::GetSession() const { return m_session; } +void ProductEditorContext::SetSelection(ProductEditorSelectionState selection) { + m_session.selection = std::move(selection); +} + +void ProductEditorContext::ClearSelection() { + m_session.selection = {}; +} + UIEditorWorkspaceController& ProductEditorContext::GetWorkspaceController() { return m_workspaceController; } @@ -110,6 +119,9 @@ void ProductEditorContext::SetReadyStatus() { void ProductEditorContext::SetStatus( std::string status, std::string message) { + if (m_lastStatus != status || m_lastMessage != message) { + AppendConsoleEntry(status, message); + } m_lastStatus = std::move(status); m_lastMessage = std::move(message); } @@ -172,6 +184,18 @@ void ProductEditorContext::UpdateStatusFromShellResult( } } +void ProductEditorContext::AppendConsoleEntry( + std::string channel, + std::string message) { + ProductEditorConsoleEntry entry = {}; + entry.channel = std::move(channel); + entry.message = std::move(message); + m_session.consoleEntries.push_back(std::move(entry)); + if (m_session.consoleEntries.size() > kMaxConsoleEntries) { + m_session.consoleEntries.erase(m_session.consoleEntries.begin()); + } +} + std::string ProductEditorContext::DescribeWorkspaceState( const UIEditorShellInteractionState& interactionState) const { std::ostringstream stream = {}; diff --git a/new_editor/app/Core/ProductEditorContext.h b/new_editor/app/Core/ProductEditorContext.h index 31d1bd80..fc7cd121 100644 --- a/new_editor/app/Core/ProductEditorContext.h +++ b/new_editor/app/Core/ProductEditorContext.h @@ -26,6 +26,8 @@ public: const std::string& GetValidationMessage() const; const EditorShellAsset& GetShellAsset() const; const ProductEditorSession& GetSession() const; + void SetSelection(ProductEditorSelectionState selection); + void ClearSelection(); UIEditorWorkspaceController& GetWorkspaceController(); const UIEditorWorkspaceController& GetWorkspaceController() const; @@ -41,6 +43,8 @@ public: const UIEditorShellInteractionState& interactionState) const; private: + void AppendConsoleEntry(std::string channel, std::string message); + EditorShellAsset m_shellAsset = {}; EditorShellAssetValidationResult m_shellValidation = {}; UIEditorWorkspaceController m_workspaceController = {}; diff --git a/new_editor/app/Core/ProductEditorSession.cpp b/new_editor/app/Core/ProductEditorSession.cpp index 9d8b7db7..3e651422 100644 --- a/new_editor/app/Core/ProductEditorSession.cpp +++ b/new_editor/app/Core/ProductEditorSession.cpp @@ -35,6 +35,18 @@ std::string_view GetProductEditorActionRouteName(ProductEditorActionRoute route) } } +std::string_view GetProductEditorSelectionKindName(ProductEditorSelectionKind kind) { + switch (kind) { + case ProductEditorSelectionKind::HierarchyNode: + return "HierarchyNode"; + case ProductEditorSelectionKind::ProjectItem: + return "ProjectItem"; + case ProductEditorSelectionKind::None: + default: + return "None"; + } +} + ProductEditorActionRoute ResolveProductEditorActionRoute(std::string_view panelId) { if (panelId == "hierarchy") { return ProductEditorActionRoute::Hierarchy; diff --git a/new_editor/app/Core/ProductEditorSession.h b/new_editor/app/Core/ProductEditorSession.h index e7fea5e8..70261513 100644 --- a/new_editor/app/Core/ProductEditorSession.h +++ b/new_editor/app/Core/ProductEditorSession.h @@ -25,16 +25,38 @@ enum class ProductEditorActionRoute : std::uint8_t { Game }; +enum class ProductEditorSelectionKind : std::uint8_t { + None = 0, + HierarchyNode, + ProjectItem +}; + +struct ProductEditorSelectionState { + ProductEditorSelectionKind kind = ProductEditorSelectionKind::None; + std::string itemId = {}; + std::string displayName = {}; + std::filesystem::path absolutePath = {}; + bool directory = false; +}; + +struct ProductEditorConsoleEntry { + std::string channel = {}; + std::string message = {}; +}; + struct ProductEditorSession { std::filesystem::path repoRoot = {}; std::filesystem::path projectRoot = {}; std::string activePanelId = {}; ProductEditorRuntimeMode runtimeMode = ProductEditorRuntimeMode::Edit; ProductEditorActionRoute activeRoute = ProductEditorActionRoute::None; + ProductEditorSelectionState selection = {}; + std::vector consoleEntries = {}; }; std::string_view GetProductEditorRuntimeModeName(ProductEditorRuntimeMode mode); std::string_view GetProductEditorActionRouteName(ProductEditorActionRoute route); +std::string_view GetProductEditorSelectionKindName(ProductEditorSelectionKind kind); ProductEditorActionRoute ResolveProductEditorActionRoute(std::string_view panelId); diff --git a/new_editor/app/Hierarchy/ProductHierarchyModel.cpp b/new_editor/app/Hierarchy/ProductHierarchyModel.cpp new file mode 100644 index 00000000..0c1d9772 --- /dev/null +++ b/new_editor/app/Hierarchy/ProductHierarchyModel.cpp @@ -0,0 +1,293 @@ +#include "Hierarchy/ProductHierarchyModel.h" + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +const ProductHierarchyNode* FindNodeRecursive( + const std::vector& nodes, + std::string_view nodeId) { + for (const ProductHierarchyNode& node : nodes) { + if (node.nodeId == nodeId) { + return &node; + } + if (const ProductHierarchyNode* child = + FindNodeRecursive(node.children, nodeId); + child != nullptr) { + return child; + } + } + return nullptr; +} + +ProductHierarchyNode* FindNodeRecursive( + std::vector& nodes, + std::string_view nodeId) { + for (ProductHierarchyNode& node : nodes) { + if (node.nodeId == nodeId) { + return &node; + } + if (ProductHierarchyNode* child = + FindNodeRecursive(node.children, nodeId); + child != nullptr) { + return child; + } + } + return nullptr; +} + +bool FindNodeParentRecursive( + const std::vector& nodes, + std::string_view nodeId, + const ProductHierarchyNode*& parent) { + for (const ProductHierarchyNode& node : nodes) { + for (const ProductHierarchyNode& child : node.children) { + if (child.nodeId == nodeId) { + parent = &node; + return true; + } + } + + if (FindNodeParentRecursive(node.children, nodeId, parent)) { + return true; + } + } + return false; +} + +bool ExtractNodeRecursive( + std::vector& nodes, + std::string_view nodeId, + ProductHierarchyNode& extractedNode) { + for (std::size_t index = 0u; index < nodes.size(); ++index) { + if (nodes[index].nodeId == nodeId) { + extractedNode = std::move(nodes[index]); + nodes.erase(nodes.begin() + static_cast(index)); + return true; + } + + if (ExtractNodeRecursive(nodes[index].children, nodeId, extractedNode)) { + return true; + } + } + return false; +} + +void BuildTreeItemsRecursive( + const std::vector& nodes, + std::uint32_t depth, + const ::XCEngine::UI::UITextureHandle& icon, + std::vector& items) { + for (const ProductHierarchyNode& node : nodes) { + Widgets::UIEditorTreeViewItem item = {}; + item.itemId = node.nodeId; + item.label = node.label; + item.depth = depth; + item.forceLeaf = node.children.empty(); + item.leadingIcon = icon; + items.push_back(std::move(item)); + BuildTreeItemsRecursive(node.children, depth + 1u, icon, items); + } +} + +} // namespace + +ProductHierarchyModel ProductHierarchyModel::BuildDefault() { + ProductHierarchyModel model = {}; + model.m_roots = { + ProductHierarchyNode{ "main_camera", "Main Camera", {} }, + ProductHierarchyNode{ "directional_light", "Directional Light", {} }, + ProductHierarchyNode{ + "player", + "Player", + { + ProductHierarchyNode{ "camera_pivot", "Camera Pivot", {} }, + ProductHierarchyNode{ "player_mesh", "Mesh", {} } + } }, + ProductHierarchyNode{ + "environment", + "Environment", + { + ProductHierarchyNode{ "ground", "Ground", {} }, + ProductHierarchyNode{ + "props", + "Props", + { + ProductHierarchyNode{ "crate_01", "Crate_01", {} }, + ProductHierarchyNode{ "barrel_01", "Barrel_01", {} } + } } + } } + }; + model.m_nextGeneratedNodeId = 1u; + return model; +} + +bool ProductHierarchyModel::Empty() const { + return m_roots.empty(); +} + +bool ProductHierarchyModel::ContainsNode(std::string_view nodeId) const { + return FindNode(nodeId) != nullptr; +} + +const ProductHierarchyNode* ProductHierarchyModel::FindNode(std::string_view nodeId) const { + return FindNodeRecursive(m_roots, nodeId); +} + +ProductHierarchyNode* ProductHierarchyModel::FindNode(std::string_view nodeId) { + return FindNodeRecursive(m_roots, nodeId); +} + +std::optional ProductHierarchyModel::GetParentId(std::string_view nodeId) const { + const ProductHierarchyNode* parent = nullptr; + if (!FindNodeParentRecursive(m_roots, nodeId, parent) || parent == nullptr) { + return std::nullopt; + } + + return parent->nodeId; +} + +bool ProductHierarchyModel::RenameNode(std::string_view nodeId, std::string label) { + ProductHierarchyNode* node = FindNode(nodeId); + if (node == nullptr || label.empty() || node->label == label) { + return false; + } + + node->label = std::move(label); + return true; +} + +std::string ProductHierarchyModel::CreateChild( + std::string_view parentId, + std::string_view label) { + ProductHierarchyNode* parent = FindNode(parentId); + if (parent == nullptr) { + return {}; + } + + ProductHierarchyNode node = {}; + node.nodeId = AllocateNodeId(); + node.label = label.empty() ? std::string("GameObject") : std::string(label); + parent->children.push_back(std::move(node)); + return parent->children.back().nodeId; +} + +bool ProductHierarchyModel::DeleteNode(std::string_view nodeId) { + if (nodeId.empty()) { + return false; + } + + ProductHierarchyNode removed = {}; + return ExtractNodeRecursive(m_roots, nodeId, removed); +} + +bool ProductHierarchyModel::CanReparent( + std::string_view sourceNodeId, + std::string_view targetParentId) const { + if (sourceNodeId.empty()) { + return false; + } + + const ProductHierarchyNode* source = FindNode(sourceNodeId); + if (source == nullptr) { + return false; + } + + if (targetParentId.empty()) { + return true; + } + + const ProductHierarchyNode* targetParent = FindNode(targetParentId); + return CanAdopt(sourceNodeId, targetParent); +} + +bool ProductHierarchyModel::Reparent( + std::string_view sourceNodeId, + std::string_view targetParentId) { + if (!CanReparent(sourceNodeId, targetParentId) || targetParentId.empty()) { + return false; + } + + const std::optional currentParentId = GetParentId(sourceNodeId); + if (currentParentId.has_value() && currentParentId.value() == targetParentId) { + return false; + } + + ProductHierarchyNode movedNode = {}; + if (!ExtractNodeRecursive(m_roots, sourceNodeId, movedNode)) { + return false; + } + + ProductHierarchyNode* targetParent = FindNode(targetParentId); + if (targetParent == nullptr) { + return false; + } + + targetParent->children.push_back(std::move(movedNode)); + return true; +} + +bool ProductHierarchyModel::MoveToRoot(std::string_view sourceNodeId) { + if (sourceNodeId.empty() || !ContainsNode(sourceNodeId)) { + return false; + } + + if (!GetParentId(sourceNodeId).has_value()) { + return false; + } + + ProductHierarchyNode movedNode = {}; + if (!ExtractNodeRecursive(m_roots, sourceNodeId, movedNode)) { + return false; + } + + m_roots.push_back(std::move(movedNode)); + return true; +} + +std::vector ProductHierarchyModel::BuildTreeItems( + const ::XCEngine::UI::UITextureHandle& icon) const { + std::vector items = {}; + BuildTreeItemsRecursive(m_roots, 0u, icon, items); + return items; +} + +std::string ProductHierarchyModel::AllocateNodeId() { + std::ostringstream stream = {}; + stream << "generated_node_" << m_nextGeneratedNodeId++; + return stream.str(); +} + +bool ProductHierarchyModel::CanAdopt( + std::string_view sourceNodeId, + const ProductHierarchyNode* targetParent) const { + if (targetParent == nullptr || sourceNodeId == targetParent->nodeId) { + return false; + } + + const ProductHierarchyNode* source = FindNode(sourceNodeId); + if (source == nullptr) { + return false; + } + + return !ContainsDescendant(*source, targetParent->nodeId); +} + +bool ProductHierarchyModel::ContainsDescendant( + const ProductHierarchyNode& node, + std::string_view candidateId) const { + for (const ProductHierarchyNode& child : node.children) { + if (child.nodeId == candidateId || ContainsDescendant(child, candidateId)) { + return true; + } + } + return false; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Hierarchy/ProductHierarchyModel.h b/new_editor/app/Hierarchy/ProductHierarchyModel.h new file mode 100644 index 00000000..225ee65a --- /dev/null +++ b/new_editor/app/Hierarchy/ProductHierarchyModel.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +struct ProductHierarchyNode { + std::string nodeId = {}; + std::string label = {}; + std::vector children = {}; +}; + +class ProductHierarchyModel { +public: + static ProductHierarchyModel BuildDefault(); + + bool Empty() const; + bool ContainsNode(std::string_view nodeId) const; + const ProductHierarchyNode* FindNode(std::string_view nodeId) const; + ProductHierarchyNode* FindNode(std::string_view nodeId); + + std::optional GetParentId(std::string_view nodeId) const; + bool RenameNode(std::string_view nodeId, std::string label); + std::string CreateChild(std::string_view parentId, std::string_view label); + bool DeleteNode(std::string_view nodeId); + + bool CanReparent(std::string_view sourceNodeId, std::string_view targetParentId) const; + bool Reparent(std::string_view sourceNodeId, std::string_view targetParentId); + bool MoveToRoot(std::string_view sourceNodeId); + + std::vector BuildTreeItems( + const ::XCEngine::UI::UITextureHandle& icon) const; + +private: + std::string AllocateNodeId(); + bool CanAdopt( + std::string_view sourceNodeId, + const ProductHierarchyNode* targetParent) const; + bool ContainsDescendant( + const ProductHierarchyNode& node, + std::string_view candidateId) const; + + std::vector m_roots = {}; + std::uint64_t m_nextGeneratedNodeId = 1u; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Panels/ProductConsolePanel.cpp b/new_editor/app/Panels/ProductConsolePanel.cpp new file mode 100644 index 00000000..39dd7145 --- /dev/null +++ b/new_editor/app/Panels/ProductConsolePanel.cpp @@ -0,0 +1,109 @@ +#include "ProductConsolePanel.h" + +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::UI::UIColor; +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; + +constexpr std::string_view kConsolePanelId = "console"; +constexpr float kPadding = 8.0f; +constexpr float kLineHeight = 18.0f; +constexpr float kFontSize = 11.0f; + +constexpr UIColor kSurfaceColor(0.205f, 0.205f, 0.205f, 1.0f); +constexpr UIColor kTextColor(0.840f, 0.840f, 0.840f, 1.0f); +constexpr UIColor kChannelColor(0.640f, 0.640f, 0.640f, 1.0f); +constexpr UIColor kEmptyColor(0.580f, 0.580f, 0.580f, 1.0f); + +float ResolveTextTop(float rectY, float rectHeight, float fontSize) { + const float lineHeight = fontSize * 1.6f; + return rectY + std::floor((rectHeight - lineHeight) * 0.5f); +} + +} // namespace + +const UIEditorPanelContentHostPanelState* ProductConsolePanel::FindMountedConsolePanel( + const UIEditorPanelContentHostFrame& contentHostFrame) const { + for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) { + if (panelState.panelId == kConsolePanelId && panelState.mounted) { + return &panelState; + } + } + + return nullptr; +} + +void ProductConsolePanel::Update( + const ProductEditorSession& session, + const UIEditorPanelContentHostFrame& contentHostFrame) { + const UIEditorPanelContentHostPanelState* panelState = + FindMountedConsolePanel(contentHostFrame); + if (panelState == nullptr) { + m_visible = false; + m_bounds = {}; + m_entries = nullptr; + return; + } + + m_visible = true; + m_bounds = panelState->bounds; + m_entries = &session.consoleEntries; +} + +void ProductConsolePanel::Append(UIDrawList& drawList) const { + if (!m_visible || m_bounds.width <= 0.0f || m_bounds.height <= 0.0f) { + return; + } + + drawList.AddFilledRect(m_bounds, kSurfaceColor); + + const UIRect contentRect( + m_bounds.x + kPadding, + m_bounds.y + kPadding, + (std::max)(m_bounds.width - kPadding * 2.0f, 0.0f), + (std::max)(m_bounds.height - kPadding * 2.0f, 0.0f)); + + if (m_entries == nullptr || m_entries->empty()) { + drawList.AddText( + UIPoint(contentRect.x, ResolveTextTop(contentRect.y, kLineHeight, kFontSize)), + "Console is empty.", + kEmptyColor, + kFontSize); + return; + } + + const std::size_t maxVisibleLines = (std::max)( + static_cast(1), + static_cast(contentRect.height / kLineHeight)); + const std::size_t entryCount = m_entries->size(); + const std::size_t firstVisible = + entryCount > maxVisibleLines ? entryCount - maxVisibleLines : 0u; + + drawList.PushClipRect(contentRect); + float nextY = contentRect.y; + for (std::size_t index = firstVisible; index < entryCount; ++index) { + const ProductEditorConsoleEntry& entry = (*m_entries)[index]; + const std::string line = entry.channel.empty() + ? entry.message + : "[" + entry.channel + "] " + entry.message; + + drawList.AddText( + UIPoint(contentRect.x, ResolveTextTop(nextY, kLineHeight, kFontSize)), + line, + entry.channel.empty() ? kTextColor : kChannelColor, + kFontSize); + nextY += kLineHeight; + } + drawList.PopClipRect(); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Panels/ProductConsolePanel.h b/new_editor/app/Panels/ProductConsolePanel.h new file mode 100644 index 00000000..ce373deb --- /dev/null +++ b/new_editor/app/Panels/ProductConsolePanel.h @@ -0,0 +1,27 @@ +#pragma once + +#include "Core/ProductEditorSession.h" + +#include + +#include + +namespace XCEngine::UI::Editor::App { + +class ProductConsolePanel { +public: + void Update( + const ProductEditorSession& session, + const UIEditorPanelContentHostFrame& contentHostFrame); + void Append(::XCEngine::UI::UIDrawList& drawList) const; + +private: + const UIEditorPanelContentHostPanelState* FindMountedConsolePanel( + const UIEditorPanelContentHostFrame& contentHostFrame) const; + + bool m_visible = false; + ::XCEngine::UI::UIRect m_bounds = {}; + const std::vector* m_entries = nullptr; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Panels/ProductHierarchyPanel.cpp b/new_editor/app/Panels/ProductHierarchyPanel.cpp index 04916eaf..7d8ab331 100644 --- a/new_editor/app/Panels/ProductHierarchyPanel.cpp +++ b/new_editor/app/Panels/ProductHierarchyPanel.cpp @@ -5,6 +5,8 @@ #include +#include +#include #include #include @@ -12,15 +14,25 @@ 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::UIPointerButton; using ::XCEngine::UI::UIRect; using Widgets::AppendUIEditorTreeViewBackground; using Widgets::AppendUIEditorTreeViewForeground; +using Widgets::DoesUIEditorTreeViewItemHaveChildren; +using Widgets::HitTestUIEditorTreeView; +using Widgets::IsUIEditorTreeViewPointInside; +using Widgets::UIEditorTreeViewHitTarget; +using Widgets::UIEditorTreeViewHitTargetKind; +using Widgets::UIEditorTreeViewInvalidIndex; constexpr std::string_view kHierarchyPanelId = "hierarchy"; +constexpr float kDragThreshold = 4.0f; +constexpr UIColor kDragPreviewColor(0.82f, 0.82f, 0.82f, 0.55f); bool ContainsPoint(const UIRect& rect, const UIPoint& point) { return point.x >= rect.x && @@ -29,12 +41,26 @@ bool ContainsPoint(const UIRect& rect, const UIPoint& point) { point.y <= rect.y + rect.height; } +float ComputeSquaredDistance(const UIPoint& lhs, const UIPoint& rhs) { + const float dx = lhs.x - rhs.x; + const float dy = lhs.y - rhs.y; + return dx * dx + dy * dy; +} + +::XCEngine::UI::UITextureHandle ResolveGameObjectIcon( + const ProductBuiltInIcons* icons) { + return icons != nullptr + ? icons->Resolve(ProductBuiltInIconKind::GameObject) + : ::XCEngine::UI::UITextureHandle {}; +} + std::vector FilterHierarchyInputEvents( const UIRect& bounds, const std::vector& inputEvents, bool allowInteraction, - bool panelActive) { - if (!allowInteraction) { + bool panelActive, + bool captureActive) { + if (!allowInteraction && !captureActive) { return {}; } @@ -46,7 +72,7 @@ std::vector FilterHierarchyInputEvents( case UIInputEventType::PointerButtonDown: case UIInputEventType::PointerButtonUp: case UIInputEventType::PointerWheel: - if (ContainsPoint(bounds, event.position)) { + if (captureActive || ContainsPoint(bounds, event.position)) { filteredEvents.push_back(event); } break; @@ -55,6 +81,10 @@ std::vector FilterHierarchyInputEvents( break; case UIInputEventType::FocusGained: case UIInputEventType::FocusLost: + if (panelActive || captureActive) { + filteredEvents.push_back(event); + } + break; case UIInputEventType::KeyDown: case UIInputEventType::KeyUp: case UIInputEventType::Character: @@ -70,16 +100,41 @@ std::vector FilterHierarchyInputEvents( return filteredEvents; } -::XCEngine::UI::UITextureHandle ResolveGameObjectIcon( - const ProductBuiltInIcons* icons) { - return icons != nullptr - ? icons->Resolve(ProductBuiltInIconKind::GameObject) - : ::XCEngine::UI::UITextureHandle {}; +const Widgets::UIEditorTreeViewItem* ResolveHitItem( + const Widgets::UIEditorTreeViewLayout& layout, + const std::vector& items, + const UIPoint& point, + UIEditorTreeViewHitTarget* hitTargetOutput = nullptr) { + const UIEditorTreeViewHitTarget hitTarget = HitTestUIEditorTreeView(layout, point); + if (hitTargetOutput != nullptr) { + *hitTargetOutput = hitTarget; + } + + if (hitTarget.itemIndex >= items.size()) { + return nullptr; + } + + return &items[hitTarget.itemIndex]; +} + +std::size_t FindVisibleIndexForItemId( + const Widgets::UIEditorTreeViewLayout& layout, + const std::vector& items, + std::string_view itemId) { + for (std::size_t visibleIndex = 0u; visibleIndex < layout.visibleItemIndices.size(); ++visibleIndex) { + const std::size_t itemIndex = layout.visibleItemIndices[visibleIndex]; + if (itemIndex < items.size() && items[itemIndex].itemId == itemId) { + return visibleIndex; + } + } + + return UIEditorTreeViewInvalidIndex; } } // namespace void ProductHierarchyPanel::Initialize() { + m_model = ProductHierarchyModel::BuildDefault(); RebuildItems(); } @@ -99,35 +154,25 @@ const UIEditorPanelContentHostPanelState* ProductHierarchyPanel::FindMountedHier return nullptr; } +void ProductHierarchyPanel::ResetTransientState() { + m_frameEvents.clear(); + m_dragState.requestPointerCapture = false; + m_dragState.requestPointerRelease = false; +} + void ProductHierarchyPanel::RebuildItems() { const auto icon = ResolveGameObjectIcon(m_icons); const std::string previousSelection = m_selection.HasSelection() ? m_selection.GetSelectedId() : std::string(); - m_treeItems = { - { "main-camera", "Main Camera", 0u, true, 0.0f, icon }, - { "directional-light", "Directional Light", 0u, true, 0.0f, icon }, - { "player", "Player", 0u, false, 0.0f, icon }, - { "player/camera-pivot", "Camera Pivot", 1u, true, 0.0f, icon }, - { "player/mesh", "Mesh", 1u, true, 0.0f, icon }, - { "environment", "Environment", 0u, false, 0.0f, icon }, - { "environment/ground", "Ground", 1u, true, 0.0f, icon }, - { "environment/props", "Props", 1u, false, 0.0f, icon }, - { "environment/props/crate-01", "Crate_01", 2u, true, 0.0f, icon }, - { "environment/props/barrel-01", "Barrel_01", 2u, true, 0.0f, icon } - }; - + m_treeItems = m_model.BuildTreeItems(icon); m_expansion.Expand("player"); m_expansion.Expand("environment"); - m_expansion.Expand("environment/props"); + m_expansion.Expand("props"); - if (!previousSelection.empty()) { - for (const Widgets::UIEditorTreeViewItem& item : m_treeItems) { - if (item.itemId == previousSelection) { - m_selection.SetSelection(previousSelection); - return; - } - } + if (!previousSelection.empty() && m_model.ContainsNode(previousSelection)) { + m_selection.SetSelection(previousSelection); + return; } if (!m_treeItems.empty()) { @@ -135,16 +180,295 @@ void ProductHierarchyPanel::RebuildItems() { } } +void ProductHierarchyPanel::EmitSelectionEvent() { + if (!m_selection.HasSelection()) { + return; + } + + const ProductHierarchyNode* node = m_model.FindNode(m_selection.GetSelectedId()); + if (node == nullptr) { + return; + } + + Event event = {}; + event.kind = EventKind::SelectionChanged; + event.itemId = node->nodeId; + event.label = node->label; + m_frameEvents.push_back(std::move(event)); +} + +void ProductHierarchyPanel::EmitReparentEvent( + EventKind kind, + std::string itemId, + std::string targetItemId) { + Event event = {}; + event.kind = kind; + event.itemId = std::move(itemId); + event.targetItemId = std::move(targetItemId); + if (const ProductHierarchyNode* node = m_model.FindNode(event.itemId); node != nullptr) { + event.label = node->label; + } + m_frameEvents.push_back(std::move(event)); +} + +std::vector ProductHierarchyPanel::BuildInteractionInputEvents( + const std::vector& inputEvents, + const UIRect& bounds, + bool allowInteraction, + bool panelActive) const { + const std::vector 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, + BuildProductTreeViewMetrics()); + DragPreviewState preview = {}; + preview.armed = m_dragState.armed; + preview.armedItemId = m_dragState.armedItemId; + preview.pressPosition = m_dragState.pressPosition; + preview.dragging = m_dragState.dragging; + + std::vector 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 ProductHierarchyPanel::ProcessDragAndFrameEvents( + const std::vector& inputEvents, + const UIRect& bounds, + bool allowInteraction, + bool panelActive) { + const std::vector filteredEvents = FilterHierarchyInputEvents( + bounds, + inputEvents, + allowInteraction, + panelActive, + HasActivePointerCapture()); + + if (m_treeFrame.result.selectionChanged) { + 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 ProductHierarchyNode* node = m_model.FindNode(event.itemId); node != nullptr) { + event.label = node->label; + } + m_frameEvents.push_back(std::move(event)); + } + + 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; + } + } +} + void ProductHierarchyPanel::Update( const UIEditorPanelContentHostFrame& contentHostFrame, const std::vector& inputEvents, bool allowInteraction, bool panelActive) { + ResetTransientState(); + const UIEditorPanelContentHostPanelState* panelState = FindMountedHierarchyPanel(contentHostFrame); if (panelState == nullptr) { m_visible = false; m_treeFrame = {}; + m_dragState = {}; return; } @@ -153,14 +477,25 @@ void ProductHierarchyPanel::Update( } m_visible = true; + const std::vector interactionEvents = + BuildInteractionInputEvents( + inputEvents, + panelState->bounds, + allowInteraction, + panelActive); m_treeFrame = UpdateUIEditorTreeViewInteraction( m_treeInteractionState, m_selection, m_expansion, panelState->bounds, m_treeItems, - FilterHierarchyInputEvents(panelState->bounds, inputEvents, allowInteraction, panelActive), + interactionEvents, BuildProductTreeViewMetrics()); + ProcessDragAndFrameEvents( + inputEvents, + panelState->bounds, + allowInteraction, + panelActive); } void ProductHierarchyPanel::Append(UIDrawList& drawList) const { @@ -184,6 +519,50 @@ void ProductHierarchyPanel::Append(UIDrawList& drawList) const { m_treeItems, palette, metrics); + + if (!m_dragState.dragging || !m_dragState.validDropTarget) { + return; + } + + if (m_dragState.dropToRoot) { + drawList.AddRectOutline( + m_treeFrame.layout.bounds, + kDragPreviewColor, + 1.0f, + 0.0f); + return; + } + + const std::size_t visibleIndex = FindVisibleIndexForItemId( + m_treeFrame.layout, + m_treeItems, + m_dragState.dropTargetItemId); + if (visibleIndex == UIEditorTreeViewInvalidIndex || + visibleIndex >= m_treeFrame.layout.rowRects.size()) { + return; + } + + drawList.AddRectOutline( + m_treeFrame.layout.rowRects[visibleIndex], + kDragPreviewColor, + 1.0f, + 0.0f); +} + +bool ProductHierarchyPanel::WantsHostPointerCapture() const { + return m_dragState.requestPointerCapture; +} + +bool ProductHierarchyPanel::WantsHostPointerRelease() const { + return m_dragState.requestPointerRelease; +} + +bool ProductHierarchyPanel::HasActivePointerCapture() const { + return m_dragState.dragging; +} + +const std::vector& ProductHierarchyPanel::GetFrameEvents() const { + return m_frameEvents; } } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Panels/ProductHierarchyPanel.h b/new_editor/app/Panels/ProductHierarchyPanel.h index fbab6899..39743575 100644 --- a/new_editor/app/Panels/ProductHierarchyPanel.h +++ b/new_editor/app/Panels/ProductHierarchyPanel.h @@ -1,5 +1,7 @@ #pragma once +#include "Hierarchy/ProductHierarchyModel.h" + #include #include @@ -7,6 +9,8 @@ #include #include +#include +#include #include namespace XCEngine::UI::Editor::App { @@ -15,6 +19,21 @@ class ProductBuiltInIcons; class ProductHierarchyPanel { public: + enum class EventKind : std::uint8_t { + None = 0, + SelectionChanged, + Reparented, + MovedToRoot, + RenameRequested + }; + + struct Event { + EventKind kind = EventKind::None; + std::string itemId = {}; + std::string targetItemId = {}; + std::string label = {}; + }; + void Initialize(); void SetBuiltInIcons(const ProductBuiltInIcons* icons); void Update( @@ -23,18 +42,54 @@ public: bool allowInteraction, bool panelActive); void Append(::XCEngine::UI::UIDrawList& drawList) const; + bool WantsHostPointerCapture() const; + bool WantsHostPointerRelease() const; + bool HasActivePointerCapture() const; + const std::vector& GetFrameEvents() const; 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 RebuildItems(); + void ProcessDragAndFrameEvents( + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const ::XCEngine::UI::UIRect& bounds, + bool allowInteraction, + bool panelActive); + void EmitSelectionEvent(); + void EmitReparentEvent( + EventKind kind, + std::string itemId, + std::string targetItemId); + std::vector<::XCEngine::UI::UIInputEvent> BuildInteractionInputEvents( + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const ::XCEngine::UI::UIRect& bounds, + bool allowInteraction, + bool panelActive) const; const ProductBuiltInIcons* m_icons = nullptr; + ProductHierarchyModel m_model = {}; std::vector m_treeItems = {}; ::XCEngine::UI::Widgets::UISelectionModel m_selection = {}; ::XCEngine::UI::Widgets::UIExpansionModel m_expansion = {}; UIEditorTreeViewInteractionState m_treeInteractionState = {}; UIEditorTreeViewInteractionFrame m_treeFrame = {}; + std::vector m_frameEvents = {}; + DragState m_dragState = {}; bool m_visible = false; }; diff --git a/new_editor/app/Panels/ProductInspectorPanel.cpp b/new_editor/app/Panels/ProductInspectorPanel.cpp new file mode 100644 index 00000000..51320c12 --- /dev/null +++ b/new_editor/app/Panels/ProductInspectorPanel.cpp @@ -0,0 +1,224 @@ +#include "ProductInspectorPanel.h" + +#include + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::UI::UIColor; +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; +constexpr float kRowHeight = 21.0f; +constexpr float kLabelWidth = 88.0f; +constexpr float kTitleFontSize = 13.0f; +constexpr float kSubtitleFontSize = 11.0f; +constexpr float kSectionTitleFontSize = 11.0f; +constexpr float kRowFontSize = 11.0f; + +constexpr UIColor kSurfaceColor(0.205f, 0.205f, 0.205f, 1.0f); +constexpr UIColor kSectionHeaderColor(0.225f, 0.225f, 0.225f, 1.0f); +constexpr UIColor kSectionBodyColor(0.215f, 0.215f, 0.215f, 1.0f); +constexpr UIColor kTitleColor(0.910f, 0.910f, 0.910f, 1.0f); +constexpr UIColor kSubtitleColor(0.650f, 0.650f, 0.650f, 1.0f); +constexpr UIColor kLabelColor(0.700f, 0.700f, 0.700f, 1.0f); +constexpr UIColor kValueColor(0.860f, 0.860f, 0.860f, 1.0f); + +float ResolveTextTop(float rectY, float rectHeight, float fontSize) { + const float lineHeight = fontSize * 1.6f; + return rectY + std::floor((rectHeight - lineHeight) * 0.5f); +} + +std::string PathToUtf8String(const std::filesystem::path& path) { + const std::u8string value = path.u8string(); + return std::string(value.begin(), value.end()); +} + +} // namespace + +const UIEditorPanelContentHostPanelState* ProductInspectorPanel::FindMountedInspectorPanel( + const UIEditorPanelContentHostFrame& contentHostFrame) const { + for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) { + if (panelState.panelId == kInspectorPanelId && panelState.mounted) { + return &panelState; + } + } + + return nullptr; +} + +void ProductInspectorPanel::BuildPresentation(const ProductEditorSession& session) { + m_sections.clear(); + m_title.clear(); + m_subtitle.clear(); + m_hasSelection = false; + + switch (session.selection.kind) { + case ProductEditorSelectionKind::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 ProductEditorSelectionKind::ProjectItem: { + m_hasSelection = true; + m_title = session.selection.displayName.empty() + ? (session.selection.directory ? std::string("Folder") : std::string("Asset")) + : session.selection.displayName; + m_subtitle = session.selection.directory ? "Folder" : "Asset"; + + Section identity = {}; + identity.title = "Identity"; + identity.rows = { + { "Type", session.selection.directory ? std::string("Folder") : std::string("Asset") }, + { "Name", m_title }, + { "Id", session.selection.itemId } + }; + m_sections.push_back(std::move(identity)); + + Section location = {}; + location.title = "Location"; + location.rows = { + { "Path", PathToUtf8String(session.selection.absolutePath) } + }; + m_sections.push_back(std::move(location)); + break; + } + + case ProductEditorSelectionKind::None: + default: + m_title = "Nothing selected"; + m_subtitle = "Select a hierarchy item or project asset."; + break; + } +} + +void ProductInspectorPanel::Update( + const ProductEditorSession& session, + const UIEditorPanelContentHostFrame& contentHostFrame) { + const UIEditorPanelContentHostPanelState* panelState = + FindMountedInspectorPanel(contentHostFrame); + if (panelState == nullptr) { + m_visible = false; + m_bounds = {}; + m_sections.clear(); + m_title.clear(); + m_subtitle.clear(); + m_hasSelection = false; + return; + } + + m_visible = true; + m_bounds = panelState->bounds; + BuildPresentation(session); +} + +void ProductInspectorPanel::Append(UIDrawList& drawList) const { + if (!m_visible || m_bounds.width <= 0.0f || m_bounds.height <= 0.0f) { + return; + } + + const UIColor borderColor = ResolveUIEditorDockHostPalette().splitterColor; + 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, 18.0f); + drawList.AddText( + UIPoint(titleRect.x, ResolveTextTop(titleRect.y, titleRect.height, kTitleFontSize)), + m_title, + kTitleColor, + kTitleFontSize); + nextY += titleRect.height; + + const UIRect subtitleRect(contentX, nextY, contentWidth, 16.0f); + drawList.AddText( + UIPoint(subtitleRect.x, ResolveTextTop(subtitleRect.y, subtitleRect.height, kSubtitleFontSize)), + m_subtitle, + kSubtitleColor, + kSubtitleFontSize); + nextY += subtitleRect.height + kSectionGap; + + if (!m_hasSelection) { + return; + } + + for (const Section& section : m_sections) { + const float bodyHeight = static_cast(section.rows.size()) * kRowHeight; + const UIRect headerRect(contentX, nextY, contentWidth, kHeaderHeight); + const UIRect bodyRect(contentX, headerRect.y + headerRect.height, contentWidth, bodyHeight); + + drawList.AddFilledRect(headerRect, kSectionHeaderColor); + drawList.AddFilledRect(bodyRect, kSectionBodyColor); + drawList.AddRectOutline( + UIRect(headerRect.x, headerRect.y, headerRect.width, headerRect.height + bodyRect.height), + borderColor, + 1.0f, + 0.0f); + drawList.AddText( + UIPoint(headerRect.x + 8.0f, ResolveTextTop(headerRect.y, headerRect.height, kSectionTitleFontSize)), + section.title, + kTitleColor, + kSectionTitleFontSize); + + float rowY = bodyRect.y; + for (std::size_t rowIndex = 0u; rowIndex < section.rows.size(); ++rowIndex) { + const SectionRow& row = section.rows[rowIndex]; + const UIRect rowRect(contentX, rowY, contentWidth, kRowHeight); + const UIRect labelRect(rowRect.x + 8.0f, rowRect.y, kLabelWidth, rowRect.height); + const UIRect valueRect( + labelRect.x + labelRect.width + 8.0f, + rowRect.y, + (std::max)(rowRect.width - (labelRect.width + 24.0f), 0.0f), + rowRect.height); + + if (rowIndex > 0u) { + drawList.AddFilledRect( + UIRect(rowRect.x, rowRect.y, rowRect.width, 1.0f), + borderColor); + } + + drawList.AddText( + UIPoint(labelRect.x, ResolveTextTop(labelRect.y, labelRect.height, kRowFontSize)), + row.label, + kLabelColor, + kRowFontSize); + + drawList.PushClipRect(valueRect); + drawList.AddText( + UIPoint(valueRect.x, ResolveTextTop(valueRect.y, valueRect.height, kRowFontSize)), + row.value, + kValueColor, + kRowFontSize); + drawList.PopClipRect(); + + rowY += kRowHeight; + } + + nextY = bodyRect.y + bodyRect.height + kSectionGap; + } +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Panels/ProductInspectorPanel.h b/new_editor/app/Panels/ProductInspectorPanel.h new file mode 100644 index 00000000..96b72b86 --- /dev/null +++ b/new_editor/app/Panels/ProductInspectorPanel.h @@ -0,0 +1,44 @@ +#pragma once + +#include "Core/ProductEditorSession.h" + +#include + +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +class ProductInspectorPanel { +public: + void Update( + const ProductEditorSession& session, + const UIEditorPanelContentHostFrame& contentHostFrame); + void Append(::XCEngine::UI::UIDrawList& drawList) const; + +private: + struct SectionRow { + std::string label = {}; + std::string value = {}; + }; + + struct Section { + std::string title = {}; + std::vector rows = {}; + }; + + const UIEditorPanelContentHostPanelState* FindMountedInspectorPanel( + const UIEditorPanelContentHostFrame& contentHostFrame) const; + void BuildPresentation(const ProductEditorSession& session); + + bool m_visible = false; + bool m_hasSelection = false; + ::XCEngine::UI::UIRect m_bounds = {}; + std::string m_title = {}; + std::string m_subtitle = {}; + std::vector
m_sections = {}; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Panels/ProductProjectPanel.cpp b/new_editor/app/Panels/ProductProjectPanel.cpp index dbd37a9b..8df33e4e 100644 --- a/new_editor/app/Panels/ProductProjectPanel.cpp +++ b/new_editor/app/Panels/ProductProjectPanel.cpp @@ -7,11 +7,9 @@ #include #include -#include #include #include #include -#include #include #include @@ -32,7 +30,6 @@ using Widgets::AppendUIEditorTreeViewBackground; using Widgets::AppendUIEditorTreeViewForeground; constexpr std::string_view kProjectPanelId = "project"; -constexpr std::string_view kAssetsRootId = "Assets"; constexpr std::size_t kInvalidLayoutIndex = static_cast(-1); constexpr float kBrowserHeaderHeight = 24.0f; @@ -102,122 +99,6 @@ float MeasureTextWidth( return static_cast(text.size()) * fontSize * 0.56f; } -std::string ToLowerCopy(std::string value) { - std::transform( - value.begin(), - value.end(), - value.begin(), - [](unsigned char character) { - return static_cast(std::tolower(character)); - }); - return value; -} - -std::string WideToUtf8(std::wstring_view value) { - if (value.empty()) { - return {}; - } - - const int requiredSize = WideCharToMultiByte( - CP_UTF8, - 0, - value.data(), - static_cast(value.size()), - nullptr, - 0, - nullptr, - nullptr); - if (requiredSize <= 0) { - return {}; - } - - std::string result(static_cast(requiredSize), '\0'); - WideCharToMultiByte( - CP_UTF8, - 0, - value.data(), - static_cast(value.size()), - result.data(), - requiredSize, - nullptr, - nullptr); - return result; -} - -std::string PathToUtf8String(const std::filesystem::path& path) { - return WideToUtf8(path.native()); -} - -std::string NormalizePathSeparators(std::string value) { - std::replace(value.begin(), value.end(), '\\', '/'); - return value; -} - -std::string BuildRelativeItemId( - const std::filesystem::path& path, - const std::filesystem::path& assetsRoot) { - const std::filesystem::path relative = - std::filesystem::relative(path, assetsRoot.parent_path()); - const std::string normalized = - NormalizePathSeparators(PathToUtf8String(relative.lexically_normal())); - return normalized.empty() ? std::string(kAssetsRootId) : normalized; -} - -std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory) { - if (directory) { - return PathToUtf8String(path.filename()); - } - - const std::string filename = PathToUtf8String(path.filename()); - const std::size_t extensionOffset = filename.find_last_of('.'); - if (extensionOffset == std::string::npos || extensionOffset == 0u) { - return filename; - } - - return filename.substr(0u, extensionOffset); -} - -bool IsMetaFile(const std::filesystem::path& path) { - return ToLowerCopy(path.extension().string()) == ".meta"; -} - -bool HasChildDirectories(const std::filesystem::path& folderPath) { - std::error_code errorCode = {}; - const std::filesystem::directory_iterator end = {}; - for (std::filesystem::directory_iterator iterator(folderPath, errorCode); - !errorCode && iterator != end; - iterator.increment(errorCode)) { - if (iterator->is_directory(errorCode)) { - return true; - } - } - - return false; -} - -std::vector CollectSortedChildDirectories( - const std::filesystem::path& folderPath) { - std::vector paths = {}; - std::error_code errorCode = {}; - const std::filesystem::directory_iterator end = {}; - for (std::filesystem::directory_iterator iterator(folderPath, errorCode); - !errorCode && iterator != end; - iterator.increment(errorCode)) { - if (iterator->is_directory(errorCode)) { - paths.push_back(iterator->path()); - } - } - - std::sort( - paths.begin(), - paths.end(), - [](const std::filesystem::path& lhs, const std::filesystem::path& rhs) { - return ToLowerCopy(PathToUtf8String(lhs.filename())) < - ToLowerCopy(PathToUtf8String(rhs.filename())); - }); - return paths; -} - std::vector FilterProjectPanelInputEvents( const UIRect& bounds, const std::vector& inputEvents, @@ -303,35 +184,6 @@ float ClampNavigationWidth(float value, float totalWidth) { return std::clamp(value, kNavigationMinWidth, maxWidth); } -std::vector BuildBreadcrumbSegments(std::string_view currentFolderId) { - std::vector segments = {}; - if (currentFolderId.empty()) { - segments.push_back(std::string(kAssetsRootId)); - return segments; - } - - std::size_t segmentStart = 0u; - while (segmentStart < currentFolderId.size()) { - const std::size_t separator = currentFolderId.find('/', segmentStart); - const std::size_t segmentLength = - separator == std::string_view::npos - ? currentFolderId.size() - segmentStart - : separator - segmentStart; - if (segmentLength > 0u) { - segments.emplace_back(currentFolderId.substr(segmentStart, segmentLength)); - } - if (separator == std::string_view::npos) { - break; - } - segmentStart = separator + 1u; - } - - if (segments.empty()) { - segments.push_back(std::string(kAssetsRootId)); - } - return segments; -} - void AppendTilePreview( UIDrawList& drawList, const UIRect& previewRect, @@ -371,18 +223,14 @@ void AppendTilePreview( } // namespace void ProductProjectPanel::Initialize(const std::filesystem::path& repoRoot) { - m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal(); - RefreshFolderTree(); + m_browserModel.Initialize(repoRoot); SyncCurrentFolderSelection(); - RefreshAssetList(); } void ProductProjectPanel::SetBuiltInIcons(const ProductBuiltInIcons* icons) { m_icons = icons; - if (!m_assetsRootPath.empty()) { - RefreshFolderTree(); - SyncCurrentFolderSelection(); - } + m_browserModel.SetFolderIcon(ResolveFolderIcon(m_icons)); + SyncCurrentFolderSelection(); } void ProductProjectPanel::SetTextMeasurer(const UIEditorTextMeasurer* textMeasurer) { @@ -411,24 +259,12 @@ const std::vector& ProductProjectPanel::GetFrameEven const ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry( std::string_view itemId) const { - for (const FolderEntry& entry : m_folderEntries) { - if (entry.itemId == itemId) { - return &entry; - } - } - - return nullptr; + return m_browserModel.FindFolderEntry(itemId); } const ProductProjectPanel::AssetEntry* ProductProjectPanel::FindAssetEntry( std::string_view itemId) const { - for (const AssetEntry& entry : m_assetEntries) { - if (entry.itemId == itemId) { - return &entry; - } - } - - return nullptr; + return m_browserModel.FindAssetEntry(itemId); } const UIEditorPanelContentHostPanelState* ProductProjectPanel::FindMountedProjectPanel( @@ -444,6 +280,9 @@ const UIEditorPanelContentHostPanelState* ProductProjectPanel::FindMountedProjec ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bounds) const { Layout layout = {}; + const auto& assetEntries = m_browserModel.GetAssetEntries(); + const std::vector breadcrumbSegments = + m_browserModel.BuildBreadcrumbSegments(); const float dividerThickness = ResolveUIEditorDockHostMetrics().splitterMetrics.thickness; layout.bounds = UIRect( bounds.x, @@ -496,9 +335,7 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound const float headerRight = layout.browserHeaderRect.x + layout.browserHeaderRect.width - kHeaderHorizontalPadding; float nextItemX = layout.browserHeaderRect.x + kHeaderHorizontalPadding; - std::string cumulativeFolderId = {}; - const std::vector segments = BuildBreadcrumbSegments(m_currentFolderId); - for (std::size_t index = 0u; index < segments.size(); ++index) { + for (std::size_t index = 0u; index < breadcrumbSegments.size(); ++index) { if (index > 0u) { const float separatorWidth = MeasureTextWidth(m_textMeasurer, ">", kHeaderFontSize); @@ -519,15 +356,9 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound nextItemX += separatorWidth + kBreadcrumbSpacing; } - if (index == 0u) { - cumulativeFolderId = segments[index]; - } else { - cumulativeFolderId += "/"; - cumulativeFolderId += segments[index]; - } - + const ProductProjectPanel::BrowserModel::BreadcrumbSegment& segment = breadcrumbSegments[index]; const float labelWidth = - MeasureTextWidth(m_textMeasurer, segments[index], kHeaderFontSize); + MeasureTextWidth(m_textMeasurer, segment.label, kHeaderFontSize); const float itemWidth = labelWidth + kBreadcrumbItemPaddingX * 2.0f; const float availableWidth = headerRight - nextItemX; if (availableWidth <= 0.0f) { @@ -535,16 +366,16 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound } layout.breadcrumbItems.push_back({ - segments[index], - cumulativeFolderId, + segment.label, + segment.targetFolderId, UIRect( nextItemX, breadcrumbY, ClampNonNegative((std::min)(itemWidth, availableWidth)), breadcrumbRowHeight), false, - index + 1u != segments.size(), - index + 1u == segments.size() + !segment.current, + segment.current }); nextItemX += itemWidth + kBreadcrumbSpacing; } @@ -557,8 +388,8 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound columnCount = 1; } - layout.assetTiles.reserve(m_assetEntries.size()); - for (std::size_t index = 0; index < m_assetEntries.size(); ++index) { + layout.assetTiles.reserve(assetEntries.size()); + for (std::size_t index = 0; index < assetEntries.size(); ++index) { const int column = static_cast(index % static_cast(columnCount)); const int row = static_cast(index / static_cast(columnCount)); const float tileX = layout.gridRect.x + static_cast(column) * (kGridTileWidth + kGridTileGapX); @@ -604,85 +435,31 @@ std::size_t ProductProjectPanel::HitTestAssetTile(const UIPoint& point) const { return kInvalidLayoutIndex; } -void ProductProjectPanel::RefreshFolderTree() { - m_folderEntries.clear(); - m_treeItems.clear(); - - if (m_assetsRootPath.empty() || !std::filesystem::exists(m_assetsRootPath)) { - return; - } - - const auto appendFolderRecursive = - [&](auto&& self, const std::filesystem::path& folderPath, std::uint32_t depth) -> void { - const std::string itemId = BuildRelativeItemId(folderPath, m_assetsRootPath); - - FolderEntry folderEntry = {}; - folderEntry.itemId = itemId; - folderEntry.absolutePath = folderPath; - m_folderEntries.push_back(std::move(folderEntry)); - - Widgets::UIEditorTreeViewItem item = {}; - item.itemId = itemId; - item.label = PathToUtf8String(folderPath.filename()); - item.depth = depth; - item.forceLeaf = !HasChildDirectories(folderPath); - item.leadingIcon = ResolveFolderIcon(m_icons); - m_treeItems.push_back(std::move(item)); - - const std::vector childFolders = - CollectSortedChildDirectories(folderPath); - for (const std::filesystem::path& childPath : childFolders) { - self(self, childPath, depth + 1u); - } - }; - - appendFolderRecursive(appendFolderRecursive, m_assetsRootPath, 0u); -} - -void ProductProjectPanel::EnsureValidCurrentFolder() { - if (m_currentFolderId.empty()) { - m_currentFolderId = std::string(kAssetsRootId); - } - - if (FindFolderEntry(m_currentFolderId) == nullptr && !m_treeItems.empty()) { - m_currentFolderId = m_treeItems.front().itemId; - } -} - -void ProductProjectPanel::ExpandFolderAncestors(std::string_view itemId) { - const FolderEntry* folderEntry = FindFolderEntry(itemId); - if (folderEntry == nullptr) { - return; - } - - std::filesystem::path path = folderEntry->absolutePath; - while (true) { - m_folderExpansion.Expand(BuildRelativeItemId(path, m_assetsRootPath)); - if (path == m_assetsRootPath) { - break; - } - path = path.parent_path(); - } -} - void ProductProjectPanel::SyncCurrentFolderSelection() { - EnsureValidCurrentFolder(); - ExpandFolderAncestors(m_currentFolderId); - m_folderSelection.SetSelection(m_currentFolderId); + const std::string& currentFolderId = m_browserModel.GetCurrentFolderId(); + if (currentFolderId.empty()) { + m_folderSelection.ClearSelection(); + return; + } + + const std::vector ancestorFolderIds = + m_browserModel.CollectCurrentFolderAncestorIds(); + for (const std::string& ancestorFolderId : ancestorFolderIds) { + m_folderExpansion.Expand(ancestorFolderId); + } + m_folderSelection.SetSelection(currentFolderId); } bool ProductProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) { - if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) { + if (!m_browserModel.NavigateToFolder(itemId)) { return false; } - m_currentFolderId = std::string(itemId); SyncCurrentFolderSelection(); m_assetSelection.ClearSelection(); m_hoveredAssetItemId.clear(); m_lastPrimaryClickedAssetId.clear(); - RefreshAssetList(); - EmitEvent(EventKind::FolderNavigated, source, FindFolderEntry(m_currentFolderId)); + EmitEvent(EventKind::FolderNavigated, source, FindFolderEntry(m_browserModel.GetCurrentFolderId())); return true; } @@ -699,7 +476,7 @@ void ProductProjectPanel::EmitEvent( event.source = source; event.itemId = folder->itemId; event.absolutePath = folder->absolutePath; - event.displayName = PathToUtf8String(folder->absolutePath.filename()); + event.displayName = folder->label; event.directory = true; m_frameEvents.push_back(std::move(event)); } @@ -731,55 +508,6 @@ void ProductProjectPanel::EmitSelectionClearedEvent(EventSource source) { m_frameEvents.push_back(std::move(event)); } -void ProductProjectPanel::RefreshAssetList() { - EnsureValidCurrentFolder(); - - m_assetEntries.clear(); - const FolderEntry* currentFolder = FindFolderEntry(m_currentFolderId); - if (currentFolder == nullptr) { - return; - } - - std::vector entries = {}; - std::error_code errorCode = {}; - const std::filesystem::directory_iterator end = {}; - for (std::filesystem::directory_iterator iterator(currentFolder->absolutePath, errorCode); - !errorCode && iterator != end; - iterator.increment(errorCode)) { - if (!iterator->exists(errorCode) || IsMetaFile(iterator->path())) { - continue; - } - if (!iterator->is_directory(errorCode) && !iterator->is_regular_file(errorCode)) { - continue; - } - - entries.push_back(*iterator); - } - - std::sort( - entries.begin(), - entries.end(), - [](const std::filesystem::directory_entry& lhs, const std::filesystem::directory_entry& rhs) { - const bool lhsDirectory = lhs.is_directory(); - const bool rhsDirectory = rhs.is_directory(); - if (lhsDirectory != rhsDirectory) { - return lhsDirectory && !rhsDirectory; - } - - return ToLowerCopy(PathToUtf8String(lhs.path().filename())) < - ToLowerCopy(PathToUtf8String(rhs.path().filename())); - }); - - for (const std::filesystem::directory_entry& entry : entries) { - AssetEntry assetEntry = {}; - assetEntry.itemId = BuildRelativeItemId(entry.path(), m_assetsRootPath); - assetEntry.absolutePath = entry.path(); - assetEntry.displayName = BuildAssetDisplayName(entry.path(), entry.is_directory()); - assetEntry.directory = entry.is_directory(); - m_assetEntries.push_back(std::move(assetEntry)); - } -} - void ProductProjectPanel::ResetTransientFrames() { m_treeFrame = {}; m_frameEvents.clear(); @@ -811,10 +539,9 @@ void ProductProjectPanel::Update( return; } - if (m_treeItems.empty()) { - RefreshFolderTree(); + if (m_browserModel.GetTreeItems().empty()) { + m_browserModel.Refresh(); SyncCurrentFolderSelection(); - RefreshAssetList(); } m_visible = true; @@ -836,13 +563,13 @@ void ProductProjectPanel::Update( m_folderSelection, m_folderExpansion, m_layout.treeRect, - m_treeItems, + m_browserModel.GetTreeItems(), treeEvents, treeMetrics); if (m_treeFrame.result.selectionChanged && !m_treeFrame.result.selectedItemId.empty() && - m_treeFrame.result.selectedItemId != m_currentFolderId) { + m_treeFrame.result.selectedItemId != m_browserModel.GetCurrentFolderId()) { NavigateToFolder(m_treeFrame.result.selectedItemId, EventSource::Tree); m_layout = BuildLayout(panelState->bounds); } @@ -871,9 +598,10 @@ void ProductProjectPanel::Update( m_splitterDragging || ContainsPoint(m_layout.dividerRect, event.position); m_hoveredBreadcrumbIndex = HitTestBreadcrumbItem(event.position); const std::size_t hoveredAssetIndex = HitTestAssetTile(event.position); + const auto& assetEntries = m_browserModel.GetAssetEntries(); m_hoveredAssetItemId = - hoveredAssetIndex < m_assetEntries.size() - ? m_assetEntries[hoveredAssetIndex].itemId + hoveredAssetIndex < assetEntries.size() + ? assetEntries[hoveredAssetIndex].itemId : std::string(); break; } @@ -902,8 +630,9 @@ void ProductProjectPanel::Update( break; } + const auto& assetEntries = m_browserModel.GetAssetEntries(); const std::size_t hitIndex = HitTestAssetTile(event.position); - if (hitIndex >= m_assetEntries.size()) { + if (hitIndex >= assetEntries.size()) { if (m_assetSelection.HasSelection()) { m_assetSelection.ClearSelection(); EmitSelectionClearedEvent(EventSource::Background); @@ -911,7 +640,7 @@ void ProductProjectPanel::Update( break; } - const AssetEntry& assetEntry = m_assetEntries[hitIndex]; + const AssetEntry& assetEntry = assetEntries[hitIndex]; const bool alreadySelected = m_assetSelection.IsSelected(assetEntry.itemId); const bool selectionChanged = m_assetSelection.SetSelection(assetEntry.itemId); if (selectionChanged) { @@ -945,13 +674,17 @@ void ProductProjectPanel::Update( if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Right && ContainsPoint(m_layout.gridRect, event.position)) { + const auto& assetEntries = m_browserModel.GetAssetEntries(); const std::size_t hitIndex = HitTestAssetTile(event.position); - if (hitIndex >= m_assetEntries.size()) { - EmitEvent(EventKind::ContextMenuRequested, EventSource::Background, static_cast(nullptr)); + if (hitIndex >= assetEntries.size()) { + EmitEvent( + EventKind::ContextMenuRequested, + EventSource::Background, + static_cast(nullptr)); break; } - const AssetEntry& assetEntry = m_assetEntries[hitIndex]; + const AssetEntry& assetEntry = assetEntries[hitIndex]; if (!m_assetSelection.IsSelected(assetEntry.itemId)) { m_assetSelection.SetSelection(assetEntry.itemId); EmitEvent(EventKind::AssetSelected, EventSource::GridSecondary, &assetEntry); @@ -1000,6 +733,8 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const { return; } + const auto& assetEntries = m_browserModel.GetAssetEntries(); + drawList.AddFilledRect(m_layout.bounds, kSurfaceColor); drawList.AddFilledRect(m_layout.leftPaneRect, kPaneColor); drawList.AddFilledRect(m_layout.rightPaneRect, kPaneColor); @@ -1021,7 +756,7 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const { AppendUIEditorTreeViewBackground( drawList, m_treeFrame.layout, - m_treeItems, + m_browserModel.GetTreeItems(), m_folderSelection, m_treeInteractionState.treeViewState, treePalette, @@ -1029,7 +764,7 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const { AppendUIEditorTreeViewForeground( drawList, m_treeFrame.layout, - m_treeItems, + m_browserModel.GetTreeItems(), treePalette, treeMetrics); @@ -1055,11 +790,11 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const { drawList.PopClipRect(); for (const AssetTileLayout& tile : m_layout.assetTiles) { - if (tile.itemIndex >= m_assetEntries.size()) { + if (tile.itemIndex >= assetEntries.size()) { continue; } - const AssetEntry& assetEntry = m_assetEntries[tile.itemIndex]; + const AssetEntry& assetEntry = assetEntries[tile.itemIndex]; const bool selected = m_assetSelection.IsSelected(assetEntry.itemId); const bool hovered = m_hoveredAssetItemId == assetEntry.itemId; @@ -1084,7 +819,7 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const { drawList.PopClipRect(); } - if (m_assetEntries.empty()) { + if (assetEntries.empty()) { const UIRect messageRect( m_layout.gridRect.x, m_layout.gridRect.y, diff --git a/new_editor/app/Panels/ProductProjectPanel.h b/new_editor/app/Panels/ProductProjectPanel.h index bc25963e..cb3f4574 100644 --- a/new_editor/app/Panels/ProductProjectPanel.h +++ b/new_editor/app/Panels/ProductProjectPanel.h @@ -1,5 +1,7 @@ #pragma once +#include "Project/ProductProjectBrowserModel.h" + #include #include #include @@ -70,17 +72,9 @@ public: const std::vector& GetFrameEvents() const; private: - struct FolderEntry { - std::string itemId = {}; - std::filesystem::path absolutePath = {}; - }; - - struct AssetEntry { - std::string itemId = {}; - std::filesystem::path absolutePath = {}; - std::string displayName = {}; - bool directory = false; - }; + using BrowserModel = ::XCEngine::UI::Editor::App::Project::ProductProjectBrowserModel; + using FolderEntry = BrowserModel::FolderEntry; + using AssetEntry = BrowserModel::AssetEntry; struct BreadcrumbItemLayout { std::string label = {}; @@ -118,10 +112,6 @@ private: Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds) const; std::size_t HitTestBreadcrumbItem(const ::XCEngine::UI::UIPoint& point) const; std::size_t HitTestAssetTile(const ::XCEngine::UI::UIPoint& point) const; - void RefreshFolderTree(); - void RefreshAssetList(); - void EnsureValidCurrentFolder(); - void ExpandFolderAncestors(std::string_view itemId); void SyncCurrentFolderSelection(); bool NavigateToFolder(std::string_view itemId, EventSource source = EventSource::None); void EmitEvent(EventKind kind, EventSource source, const FolderEntry* folder); @@ -129,10 +119,7 @@ private: void EmitSelectionClearedEvent(EventSource source); void ResetTransientFrames(); - std::filesystem::path m_assetsRootPath = {}; - std::vector m_folderEntries = {}; - std::vector m_treeItems = {}; - std::vector m_assetEntries = {}; + BrowserModel m_browserModel = {}; const ProductBuiltInIcons* m_icons = nullptr; const ::XCEngine::UI::Editor::UIEditorTextMeasurer* m_textMeasurer = nullptr; ::XCEngine::UI::Widgets::UISelectionModel m_folderSelection = {}; @@ -142,7 +129,6 @@ private: UIEditorTreeViewInteractionFrame m_treeFrame = {}; std::vector m_frameEvents = {}; Layout m_layout = {}; - std::string m_currentFolderId = {}; std::string m_hoveredAssetItemId = {}; std::string m_lastPrimaryClickedAssetId = {}; float m_navigationWidth = 248.0f; diff --git a/new_editor/app/Project/ProductProjectBrowserModel.cpp b/new_editor/app/Project/ProductProjectBrowserModel.cpp new file mode 100644 index 00000000..418c3611 --- /dev/null +++ b/new_editor/app/Project/ProductProjectBrowserModel.cpp @@ -0,0 +1,376 @@ +#include "Project/ProductProjectBrowserModel.h" + +#include +#include +#include +#include + +#include + +namespace XCEngine::UI::Editor::App::Project { + +namespace { + +constexpr std::string_view kAssetsRootId = "Assets"; + +std::string ToLowerCopy(std::string value) { + std::transform( + value.begin(), + value.end(), + value.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); + return value; +} + +std::string WideToUtf8(std::wstring_view value) { + if (value.empty()) { + return {}; + } + + const int requiredSize = WideCharToMultiByte( + CP_UTF8, + 0, + value.data(), + static_cast(value.size()), + nullptr, + 0, + nullptr, + nullptr); + if (requiredSize <= 0) { + return {}; + } + + std::string result(static_cast(requiredSize), '\0'); + WideCharToMultiByte( + CP_UTF8, + 0, + value.data(), + static_cast(value.size()), + result.data(), + requiredSize, + nullptr, + nullptr); + return result; +} + +std::string PathToUtf8String(const std::filesystem::path& path) { + return WideToUtf8(path.native()); +} + +std::string NormalizePathSeparators(std::string value) { + std::replace(value.begin(), value.end(), '\\', '/'); + return value; +} + +std::string BuildRelativeItemId( + const std::filesystem::path& path, + const std::filesystem::path& assetsRoot) { + const std::filesystem::path relative = + std::filesystem::relative(path, assetsRoot.parent_path()); + const std::string normalized = + NormalizePathSeparators(PathToUtf8String(relative.lexically_normal())); + return normalized.empty() ? std::string(kAssetsRootId) : normalized; +} + +std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory) { + if (directory) { + return PathToUtf8String(path.filename()); + } + + const std::string filename = PathToUtf8String(path.filename()); + const std::size_t extensionOffset = filename.find_last_of('.'); + if (extensionOffset == std::string::npos || extensionOffset == 0u) { + return filename; + } + + return filename.substr(0u, extensionOffset); +} + +bool IsMetaFile(const std::filesystem::path& path) { + return ToLowerCopy(path.extension().string()) == ".meta"; +} + +bool HasChildDirectories(const std::filesystem::path& folderPath) { + std::error_code errorCode = {}; + const std::filesystem::directory_iterator end = {}; + for (std::filesystem::directory_iterator iterator(folderPath, errorCode); + !errorCode && iterator != end; + iterator.increment(errorCode)) { + if (iterator->is_directory(errorCode)) { + return true; + } + } + + return false; +} + +std::vector CollectSortedChildDirectories( + const std::filesystem::path& folderPath) { + std::vector paths = {}; + std::error_code errorCode = {}; + const std::filesystem::directory_iterator end = {}; + for (std::filesystem::directory_iterator iterator(folderPath, errorCode); + !errorCode && iterator != end; + iterator.increment(errorCode)) { + if (iterator->is_directory(errorCode)) { + paths.push_back(iterator->path()); + } + } + + std::sort( + paths.begin(), + paths.end(), + [](const std::filesystem::path& lhs, const std::filesystem::path& rhs) { + return ToLowerCopy(PathToUtf8String(lhs.filename())) < + ToLowerCopy(PathToUtf8String(rhs.filename())); + }); + return paths; +} + +} // namespace + +void ProductProjectBrowserModel::Initialize(const std::filesystem::path& repoRoot) { + m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal(); + Refresh(); +} + +void ProductProjectBrowserModel::SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon) { + m_folderIcon = icon; + if (!m_assetsRootPath.empty()) { + RefreshFolderTree(); + } +} + +void ProductProjectBrowserModel::Refresh() { + RefreshFolderTree(); + EnsureValidCurrentFolder(); + RefreshAssetList(); +} + +bool ProductProjectBrowserModel::Empty() const { + return m_treeItems.empty(); +} + +const std::filesystem::path& ProductProjectBrowserModel::GetAssetsRootPath() const { + return m_assetsRootPath; +} + +const std::vector& ProductProjectBrowserModel::GetFolderEntries() const { + return m_folderEntries; +} + +const std::vector& ProductProjectBrowserModel::GetTreeItems() const { + return m_treeItems; +} + +const std::vector& ProductProjectBrowserModel::GetAssetEntries() const { + return m_assetEntries; +} + +const std::string& ProductProjectBrowserModel::GetCurrentFolderId() const { + return m_currentFolderId; +} + +const ProductProjectBrowserModel::FolderEntry* ProductProjectBrowserModel::FindFolderEntry( + std::string_view itemId) const { + for (const FolderEntry& entry : m_folderEntries) { + if (entry.itemId == itemId) { + return &entry; + } + } + + return nullptr; +} + +const ProductProjectBrowserModel::AssetEntry* ProductProjectBrowserModel::FindAssetEntry( + std::string_view itemId) const { + for (const AssetEntry& entry : m_assetEntries) { + if (entry.itemId == itemId) { + return &entry; + } + } + + return nullptr; +} + +bool ProductProjectBrowserModel::NavigateToFolder(std::string_view itemId) { + if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) { + return false; + } + + m_currentFolderId = std::string(itemId); + EnsureValidCurrentFolder(); + RefreshAssetList(); + return true; +} + +std::vector ProductProjectBrowserModel::BuildBreadcrumbSegments() const { + std::vector segments = {}; + if (m_currentFolderId.empty()) { + segments.push_back(BreadcrumbSegment{ std::string(kAssetsRootId), std::string(kAssetsRootId), true }); + return segments; + } + + std::string cumulativeFolderId = {}; + std::size_t segmentStart = 0u; + while (segmentStart < m_currentFolderId.size()) { + const std::size_t separator = m_currentFolderId.find('/', segmentStart); + const std::size_t segmentLength = + separator == std::string_view::npos + ? m_currentFolderId.size() - segmentStart + : separator - segmentStart; + if (segmentLength > 0u) { + std::string label = std::string(m_currentFolderId.substr(segmentStart, segmentLength)); + if (cumulativeFolderId.empty()) { + cumulativeFolderId = label; + } else { + cumulativeFolderId += "/"; + cumulativeFolderId += label; + } + + segments.push_back(BreadcrumbSegment{ + std::move(label), + cumulativeFolderId, + false + }); + } + if (separator == std::string_view::npos) { + break; + } + segmentStart = separator + 1u; + } + + if (segments.empty()) { + segments.push_back(BreadcrumbSegment{ std::string(kAssetsRootId), std::string(kAssetsRootId), true }); + return segments; + } + + segments.back().current = true; + return segments; +} + +std::vector ProductProjectBrowserModel::CollectCurrentFolderAncestorIds() const { + return BuildAncestorFolderIds(m_currentFolderId); +} + +std::vector ProductProjectBrowserModel::BuildAncestorFolderIds( + std::string_view itemId) const { + std::vector ancestors = {}; + const FolderEntry* folderEntry = FindFolderEntry(itemId); + if (folderEntry == nullptr) { + return ancestors; + } + + std::filesystem::path path = folderEntry->absolutePath; + while (true) { + ancestors.push_back(BuildRelativeItemId(path, m_assetsRootPath)); + if (path == m_assetsRootPath) { + break; + } + path = path.parent_path(); + } + + std::reverse(ancestors.begin(), ancestors.end()); + return ancestors; +} + +void ProductProjectBrowserModel::RefreshFolderTree() { + m_folderEntries.clear(); + m_treeItems.clear(); + + if (m_assetsRootPath.empty() || !std::filesystem::exists(m_assetsRootPath)) { + return; + } + + const auto appendFolderRecursive = + [&](auto&& self, const std::filesystem::path& folderPath, std::uint32_t depth) -> void { + const std::string itemId = BuildRelativeItemId(folderPath, m_assetsRootPath); + + FolderEntry folderEntry = {}; + folderEntry.itemId = itemId; + folderEntry.absolutePath = folderPath; + folderEntry.label = PathToUtf8String(folderPath.filename()); + m_folderEntries.push_back(folderEntry); + + Widgets::UIEditorTreeViewItem item = {}; + item.itemId = itemId; + item.label = folderEntry.label; + item.depth = depth; + item.forceLeaf = !HasChildDirectories(folderPath); + item.leadingIcon = m_folderIcon; + m_treeItems.push_back(std::move(item)); + + const std::vector childFolders = + CollectSortedChildDirectories(folderPath); + for (const std::filesystem::path& childPath : childFolders) { + self(self, childPath, depth + 1u); + } + }; + + appendFolderRecursive(appendFolderRecursive, m_assetsRootPath, 0u); +} + +void ProductProjectBrowserModel::RefreshAssetList() { + EnsureValidCurrentFolder(); + + m_assetEntries.clear(); + const FolderEntry* currentFolder = FindFolderEntry(m_currentFolderId); + if (currentFolder == nullptr) { + return; + } + + std::vector entries = {}; + std::error_code errorCode = {}; + const std::filesystem::directory_iterator end = {}; + for (std::filesystem::directory_iterator iterator(currentFolder->absolutePath, errorCode); + !errorCode && iterator != end; + iterator.increment(errorCode)) { + if (!iterator->exists(errorCode) || IsMetaFile(iterator->path())) { + continue; + } + if (!iterator->is_directory(errorCode) && !iterator->is_regular_file(errorCode)) { + continue; + } + + entries.push_back(*iterator); + } + + std::sort( + entries.begin(), + entries.end(), + [](const std::filesystem::directory_entry& lhs, const std::filesystem::directory_entry& rhs) { + const bool lhsDirectory = lhs.is_directory(); + const bool rhsDirectory = rhs.is_directory(); + if (lhsDirectory != rhsDirectory) { + return lhsDirectory && !rhsDirectory; + } + + return ToLowerCopy(PathToUtf8String(lhs.path().filename())) < + ToLowerCopy(PathToUtf8String(rhs.path().filename())); + }); + + for (const std::filesystem::directory_entry& entry : entries) { + AssetEntry assetEntry = {}; + assetEntry.itemId = BuildRelativeItemId(entry.path(), m_assetsRootPath); + assetEntry.absolutePath = entry.path(); + assetEntry.displayName = BuildAssetDisplayName(entry.path(), entry.is_directory()); + assetEntry.directory = entry.is_directory(); + m_assetEntries.push_back(std::move(assetEntry)); + } +} + +void ProductProjectBrowserModel::EnsureValidCurrentFolder() { + if (m_currentFolderId.empty()) { + m_currentFolderId = std::string(kAssetsRootId); + } + + if (FindFolderEntry(m_currentFolderId) == nullptr) { + m_currentFolderId = m_treeItems.empty() + ? std::string() + : m_treeItems.front().itemId; + } +} + +} // namespace XCEngine::UI::Editor::App::Project diff --git a/new_editor/app/Project/ProductProjectBrowserModel.h b/new_editor/app/Project/ProductProjectBrowserModel.h new file mode 100644 index 00000000..232516ca --- /dev/null +++ b/new_editor/app/Project/ProductProjectBrowserModel.h @@ -0,0 +1,67 @@ +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App::Project { + +class ProductProjectBrowserModel { +public: + struct BreadcrumbSegment { + std::string label = {}; + std::string targetFolderId = {}; + bool current = false; + }; + + struct FolderEntry { + std::string itemId = {}; + std::filesystem::path absolutePath = {}; + std::string label = {}; + }; + + struct AssetEntry { + std::string itemId = {}; + std::filesystem::path absolutePath = {}; + std::string displayName = {}; + bool directory = false; + }; + + void Initialize(const std::filesystem::path& repoRoot); + void SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon); + void Refresh(); + + bool Empty() const; + const std::filesystem::path& GetAssetsRootPath() const; + const std::vector& GetFolderEntries() const; + const std::vector& GetTreeItems() const; + const std::vector& GetAssetEntries() const; + const std::string& GetCurrentFolderId() const; + + const FolderEntry* FindFolderEntry(std::string_view itemId) const; + const AssetEntry* FindAssetEntry(std::string_view itemId) const; + bool NavigateToFolder(std::string_view itemId); + std::vector BuildBreadcrumbSegments() const; + std::vector CollectCurrentFolderAncestorIds() const; + std::vector BuildAncestorFolderIds(std::string_view itemId) const; + +private: + void RefreshFolderTree(); + void RefreshAssetList(); + void EnsureValidCurrentFolder(); + + std::filesystem::path m_assetsRootPath = {}; + std::vector m_folderEntries = {}; + std::vector m_treeItems = {}; + std::vector m_assetEntries = {}; + ::XCEngine::UI::UITextureHandle m_folderIcon = {}; + std::string m_currentFolderId = {}; +}; + +} // namespace XCEngine::UI::Editor::App::Project diff --git a/new_editor/app/Shell/ProductShellAsset.cpp b/new_editor/app/Shell/ProductShellAsset.cpp index 1aef9f3d..111bde88 100644 --- a/new_editor/app/Shell/ProductShellAsset.cpp +++ b/new_editor/app/Shell/ProductShellAsset.cpp @@ -23,8 +23,8 @@ UIEditorPanelRegistry BuildPanelRegistry() { { "hierarchy", "Hierarchy", UIEditorPanelPresentationKind::HostedContent, true, false, false }, { "scene", "Scene", UIEditorPanelPresentationKind::HostedContent, false, false, false }, { "game", "Game", UIEditorPanelPresentationKind::HostedContent, false, false, false }, - { "inspector", "Inspector", UIEditorPanelPresentationKind::Placeholder, true, false, false }, - { "console", "Console", UIEditorPanelPresentationKind::Placeholder, true, false, false }, + { "inspector", "Inspector", UIEditorPanelPresentationKind::HostedContent, true, false, false }, + { "console", "Console", UIEditorPanelPresentationKind::HostedContent, true, false, false }, { "project", "Project", UIEditorPanelPresentationKind::HostedContent, false, false, false } }; return registry; @@ -434,8 +434,8 @@ UIEditorShellInteractionDefinition BuildBaseShellDefinition() { BuildHostedContentPresentation("hierarchy"), BuildHostedContentPresentation("scene"), BuildHostedContentPresentation("game"), - BuildPlaceholderPresentation("inspector"), - BuildPlaceholderPresentation("console"), + BuildHostedContentPresentation("inspector"), + BuildHostedContentPresentation("console"), BuildHostedContentPresentation("project") }; return definition; diff --git a/new_editor/app/Workspace/ProductEditorWorkspace.cpp b/new_editor/app/Workspace/ProductEditorWorkspace.cpp index c3f3f1bb..da5db163 100644 --- a/new_editor/app/Workspace/ProductEditorWorkspace.cpp +++ b/new_editor/app/Workspace/ProductEditorWorkspace.cpp @@ -1,5 +1,7 @@ #include "ProductEditorWorkspace.h" +#include "Workspace/ProductEditorWorkspaceEventRouter.h" + #include #include @@ -90,6 +92,7 @@ void ProductEditorWorkspace::Initialize( void ProductEditorWorkspace::Shutdown() { m_shellFrame = {}; m_shellInteractionState = {}; + m_traceEntries.clear(); m_builtInIcons.Shutdown(); } @@ -131,6 +134,13 @@ void ProductEditorWorkspace::Update( hostedContentEvents, !m_shellFrame.result.workspaceInputSuppressed, activePanelId == "project"); + m_traceEntries = ConsumeProductEditorWorkspaceEvents(context, *this); + m_inspectorPanel.Update( + context.GetSession(), + m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame); + m_consolePanel.Update( + context.GetSession(), + m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame); } void ProductEditorWorkspace::Append(UIDrawList& drawList) const { @@ -145,7 +155,9 @@ void ProductEditorWorkspace::Append(UIDrawList& drawList) const { m_shellInteractionState.composeState, palette.shellPalette, metrics.shellMetrics); + m_consolePanel.Append(drawList); m_hierarchyPanel.Append(drawList); + m_inspectorPanel.Append(drawList); m_projectPanel.Append(drawList); AppendShellPopups(drawList, m_shellFrame, palette, metrics); } @@ -158,6 +170,14 @@ const UIEditorShellInteractionState& ProductEditorWorkspace::GetShellInteraction return m_shellInteractionState; } +const std::vector& ProductEditorWorkspace::GetTraceEntries() const { + return m_traceEntries; +} + +const std::vector& ProductEditorWorkspace::GetHierarchyPanelEvents() const { + return m_hierarchyPanel.GetFrameEvents(); +} + const std::vector& ProductEditorWorkspace::GetProjectPanelEvents() const { return m_projectPanel.GetFrameEvents(); } @@ -176,15 +196,19 @@ Widgets::UIEditorDockHostCursorKind ProductEditorWorkspace::GetDockCursorKind() } bool ProductEditorWorkspace::WantsHostPointerCapture() const { - return m_projectPanel.WantsHostPointerCapture(); + return m_hierarchyPanel.WantsHostPointerCapture() || + m_projectPanel.WantsHostPointerCapture(); } bool ProductEditorWorkspace::WantsHostPointerRelease() const { - return m_projectPanel.WantsHostPointerRelease(); + return (m_hierarchyPanel.WantsHostPointerRelease() || + m_projectPanel.WantsHostPointerRelease()) && + !HasHostedContentCapture(); } bool ProductEditorWorkspace::HasHostedContentCapture() const { - return m_projectPanel.HasActivePointerCapture(); + return m_hierarchyPanel.HasActivePointerCapture() || + m_projectPanel.HasActivePointerCapture(); } bool ProductEditorWorkspace::HasShellInteractiveCapture() const { diff --git a/new_editor/app/Workspace/ProductEditorWorkspace.h b/new_editor/app/Workspace/ProductEditorWorkspace.h index 83ede2b5..35aec773 100644 --- a/new_editor/app/Workspace/ProductEditorWorkspace.h +++ b/new_editor/app/Workspace/ProductEditorWorkspace.h @@ -1,9 +1,12 @@ #pragma once #include "Core/ProductEditorContext.h" +#include "Panels/ProductConsolePanel.h" #include "Icons/ProductBuiltInIcons.h" #include "Panels/ProductHierarchyPanel.h" +#include "Panels/ProductInspectorPanel.h" #include "Panels/ProductProjectPanel.h" +#include "Workspace/ProductEditorWorkspaceEventRouter.h" #include @@ -33,6 +36,8 @@ public: const UIEditorShellInteractionFrame& GetShellFrame() const; const UIEditorShellInteractionState& GetShellInteractionState() const; + const std::vector& GetTraceEntries() const; + const std::vector& GetHierarchyPanelEvents() const; const std::vector& GetProjectPanelEvents() const; const std::string& GetBuiltInIconError() const; @@ -46,10 +51,13 @@ public: private: ProductBuiltInIcons m_builtInIcons = {}; + ProductConsolePanel m_consolePanel = {}; ProductHierarchyPanel m_hierarchyPanel = {}; + ProductInspectorPanel m_inspectorPanel = {}; ProductProjectPanel m_projectPanel = {}; UIEditorShellInteractionState m_shellInteractionState = {}; UIEditorShellInteractionFrame m_shellFrame = {}; + std::vector m_traceEntries = {}; }; } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.cpp b/new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.cpp new file mode 100644 index 00000000..106d6314 --- /dev/null +++ b/new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.cpp @@ -0,0 +1,178 @@ +#include "Workspace/ProductEditorWorkspaceEventRouter.h" + +#include "Core/ProductEditorContext.h" +#include "Panels/ProductHierarchyPanel.h" +#include "Panels/ProductProjectPanel.h" +#include "Workspace/ProductEditorWorkspace.h" + +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +std::string DescribeProjectPanelEvent(const ProductProjectPanel::Event& event) { + std::ostringstream stream = {}; + switch (event.kind) { + case ProductProjectPanel::EventKind::AssetSelected: + stream << "AssetSelected"; + break; + case ProductProjectPanel::EventKind::AssetSelectionCleared: + stream << "AssetSelectionCleared"; + break; + case ProductProjectPanel::EventKind::FolderNavigated: + stream << "FolderNavigated"; + break; + case ProductProjectPanel::EventKind::AssetOpened: + stream << "AssetOpened"; + break; + case ProductProjectPanel::EventKind::ContextMenuRequested: + stream << "ContextMenuRequested"; + break; + case ProductProjectPanel::EventKind::None: + default: + stream << "None"; + break; + } + + stream << " source="; + switch (event.source) { + case ProductProjectPanel::EventSource::Tree: + stream << "Tree"; + break; + case ProductProjectPanel::EventSource::Breadcrumb: + stream << "Breadcrumb"; + break; + case ProductProjectPanel::EventSource::GridPrimary: + stream << "GridPrimary"; + break; + case ProductProjectPanel::EventSource::GridDoubleClick: + stream << "GridDoubleClick"; + break; + case ProductProjectPanel::EventSource::GridSecondary: + stream << "GridSecondary"; + break; + case ProductProjectPanel::EventSource::Background: + stream << "Background"; + break; + case ProductProjectPanel::EventSource::None: + default: + stream << "None"; + break; + } + + if (!event.itemId.empty()) { + stream << " item=" << event.itemId; + } + if (!event.displayName.empty()) { + stream << " label=" << event.displayName; + } + return stream.str(); +} + +std::string DescribeHierarchyPanelEvent(const ProductHierarchyPanel::Event& event) { + std::ostringstream stream = {}; + switch (event.kind) { + case ProductHierarchyPanel::EventKind::SelectionChanged: + stream << "SelectionChanged"; + break; + case ProductHierarchyPanel::EventKind::Reparented: + stream << "Reparented"; + break; + case ProductHierarchyPanel::EventKind::MovedToRoot: + stream << "MovedToRoot"; + break; + case ProductHierarchyPanel::EventKind::RenameRequested: + stream << "RenameRequested"; + break; + case ProductHierarchyPanel::EventKind::None: + default: + stream << "None"; + break; + } + + if (!event.itemId.empty()) { + stream << " item=" << event.itemId; + } + if (!event.targetItemId.empty()) { + stream << " target=" << event.targetItemId; + } + if (!event.label.empty()) { + stream << " label=" << event.label; + } + return stream.str(); +} + +void ApplyHierarchySelection( + ProductEditorContext& context, + const ProductHierarchyPanel::Event& event) { + if (event.kind != ProductHierarchyPanel::EventKind::SelectionChanged) { + return; + } + + if (event.itemId.empty()) { + context.ClearSelection(); + return; + } + + ProductEditorSelectionState selection = {}; + selection.kind = ProductEditorSelectionKind::HierarchyNode; + selection.itemId = event.itemId; + selection.displayName = event.label.empty() ? event.itemId : event.label; + context.SetSelection(std::move(selection)); +} + +void ApplyProjectSelection( + ProductEditorContext& context, + const ProductProjectPanel::Event& event) { + switch (event.kind) { + case ProductProjectPanel::EventKind::AssetSelected: { + ProductEditorSelectionState selection = {}; + selection.kind = ProductEditorSelectionKind::ProjectItem; + selection.itemId = event.itemId; + selection.displayName = event.displayName.empty() ? event.itemId : event.displayName; + selection.absolutePath = event.absolutePath; + selection.directory = event.directory; + context.SetSelection(std::move(selection)); + return; + } + case ProductProjectPanel::EventKind::AssetSelectionCleared: + case ProductProjectPanel::EventKind::FolderNavigated: + if (context.GetSession().selection.kind == ProductEditorSelectionKind::ProjectItem) { + context.ClearSelection(); + } + return; + case ProductProjectPanel::EventKind::AssetOpened: + case ProductProjectPanel::EventKind::ContextMenuRequested: + case ProductProjectPanel::EventKind::None: + default: + return; + } +} + +} // namespace + +std::vector ConsumeProductEditorWorkspaceEvents( + ProductEditorContext& context, + const ProductEditorWorkspace& workspace) { + std::vector entries = {}; + + for (const ProductHierarchyPanel::Event& event : workspace.GetHierarchyPanelEvents()) { + ApplyHierarchySelection(context, event); + const std::string message = DescribeHierarchyPanelEvent(event); + context.SetStatus("Hierarchy", message); + entries.push_back(ProductEditorWorkspaceTraceEntry{ "hierarchy", std::move(message) }); + } + + for (const ProductProjectPanel::Event& event : workspace.GetProjectPanelEvents()) { + ApplyProjectSelection(context, event); + const std::string message = DescribeProjectPanelEvent(event); + context.SetStatus("Project", message); + entries.push_back(ProductEditorWorkspaceTraceEntry{ "project", std::move(message) }); + } + + return entries; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.h b/new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.h new file mode 100644 index 00000000..2118713f --- /dev/null +++ b/new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace XCEngine::UI::Editor::App { + +class ProductEditorContext; +class ProductEditorWorkspace; + +struct ProductEditorWorkspaceTraceEntry { + std::string channel = {}; + std::string message = {}; +}; + +std::vector ConsumeProductEditorWorkspaceEvents( + ProductEditorContext& context, + const ProductEditorWorkspace& workspace); + +} // namespace XCEngine::UI::Editor::App