#pragma once #include "Application.h" #include "BuiltInIcons.h" #include "Core.h" #include "Core/IEditorContext.h" #include "Core/IProjectManager.h" #include "Core/ISceneManager.h" #include "SearchText.h" #include "StyleTokens.h" #include "Utils/ProjectFileUtils.h" #include "Widgets.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace XCEngine { namespace Editor { namespace UI { enum class ReferencePickerTab { Assets = 0, Scene = 1 }; struct ReferencePickerOptions { const char* popupTitle = "Select Object"; const char* emptyHint = "None"; const char* searchHint = "Search"; const char* noAssetsText = "No compatible assets."; const char* noSceneText = "No compatible scene objects."; const char* assetsTabLabel = "Assets"; const char* sceneTabLabel = "Scene"; bool showAssetsTab = true; bool showSceneTab = true; std::initializer_list supportedAssetExtensions; std::function sceneFilter; }; struct ReferencePickerInteraction { std::string assignedAssetPath; ::XCEngine::Components::GameObject::ID assignedSceneObjectId = ::XCEngine::Components::GameObject::INVALID_ID; bool clearRequested = false; }; namespace Detail { struct ReferencePickerState { ReferencePickerTab activeTab = ReferencePickerTab::Assets; std::array searchBuffer{}; }; struct ReferencePickerCandidate { std::string label; std::string secondaryLabel; std::string normalizedSearchText; AssetIconKind iconKind = AssetIconKind::File; std::string assetPath; ::XCEngine::Components::GameObject::ID sceneObjectId = ::XCEngine::Components::GameObject::INVALID_ID; float indent = 0.0f; bool IsAsset() const { return !assetPath.empty(); } bool IsSceneObject() const { return sceneObjectId != ::XCEngine::Components::GameObject::INVALID_ID; } }; inline ImVec4 ReferencePickerBackgroundColor() { return HierarchyInspectorPanelBackgroundColor(); } inline ImVec4 ReferencePickerSurfaceColor() { return ProjectBrowserPaneBackgroundColor(); } inline ImVec4 ReferencePickerBorderColor() { return ImVec4(0.32f, 0.32f, 0.32f, 1.0f); } inline ImVec4 ReferencePickerTextColor() { return ImVec4(0.84f, 0.84f, 0.84f, 1.0f); } inline ImVec4 ReferencePickerTextDisabledColor() { return ImVec4(0.60f, 0.60f, 0.60f, 1.0f); } inline ImVec4 ReferencePickerItemColor() { return ToolbarButtonColor(false); } inline ImVec4 ReferencePickerItemHoveredColor() { return ToolbarButtonHoveredColor(false); } inline ImVec4 ReferencePickerItemActiveColor() { return ToolbarButtonActiveColor(); } inline constexpr int ReferencePickerPopupStyleVarCount() { return 3; } inline constexpr int ReferencePickerPopupStyleColorCount() { return 14; } inline void PushReferencePickerPopupStyle() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, PopupWindowPadding()); ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, PopupWindowRounding()); ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, PopupWindowBorderSize()); ImGui::PushStyleColor(ImGuiCol_PopupBg, ReferencePickerBackgroundColor()); ImGui::PushStyleColor(ImGuiCol_Border, ReferencePickerBorderColor()); ImGui::PushStyleColor(ImGuiCol_Text, ReferencePickerTextColor()); ImGui::PushStyleColor(ImGuiCol_TextDisabled, ReferencePickerTextDisabledColor()); ImGui::PushStyleColor(ImGuiCol_ChildBg, ReferencePickerSurfaceColor()); ImGui::PushStyleColor(ImGuiCol_Header, ReferencePickerItemColor()); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ReferencePickerItemHoveredColor()); ImGui::PushStyleColor(ImGuiCol_HeaderActive, ReferencePickerItemActiveColor()); ImGui::PushStyleColor(ImGuiCol_FrameBg, ReferencePickerSurfaceColor()); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ReferencePickerItemHoveredColor()); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ReferencePickerItemActiveColor()); ImGui::PushStyleColor(ImGuiCol_Button, ReferencePickerItemColor()); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ReferencePickerItemHoveredColor()); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ReferencePickerItemActiveColor()); } inline void PopReferencePickerPopupStyle() { ImGui::PopStyleColor(ReferencePickerPopupStyleColorCount()); ImGui::PopStyleVar(ReferencePickerPopupStyleVarCount()); } inline bool BeginReferencePickerPopup(const char* id, const char* title) { PushReferencePickerPopupStyle(); const bool open = ImGui::BeginPopup(id); if (!open) { PopReferencePickerPopupStyle(); return false; } if (title && title[0] != '\0') { ImGui::TextUnformatted(title); ImGui::Separator(); } return true; } inline void EndReferencePickerPopup() { ImGui::EndPopup(); PopReferencePickerPopupStyle(); } inline ReferencePickerState& GetReferencePickerState(ImGuiID id) { static std::unordered_map states; return states[id]; } inline bool MatchesSupportedAssetExtension( std::string_view extensionLower, std::initializer_list supportedExtensions) { if (supportedExtensions.size() == 0) { return true; } for (const char* supportedExtension : supportedExtensions) { if (supportedExtension != nullptr && extensionLower == supportedExtension) { return true; } } return false; } inline AssetIconKind ResolveAssetIconKind(const AssetItemPtr& item) { if (!item) { return AssetIconKind::File; } if (item->isFolder) { return AssetIconKind::Folder; } if (item->type == "Scene") { return AssetIconKind::Scene; } return AssetIconKind::File; } inline AssetIconKind ResolveAssetIconKindFromPath(const std::string& path) { std::string extension = std::filesystem::path(path).extension().string(); std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); if (extension == ".xc" || extension == ".unity" || extension == ".scene") { return AssetIconKind::Scene; } return AssetIconKind::File; } inline std::string GetAssetDisplayName(const AssetItemPtr& item) { if (!item) { return {}; } if (item->isFolder) { return item->name; } return std::filesystem::path(item->name).stem().string(); } inline std::string GetAssetDisplayNameFromPath(const std::string& path) { if (path.empty()) { return {}; } return std::filesystem::path(path).stem().string(); } inline std::string BuildSceneHierarchyPath(const ::XCEngine::Components::GameObject* gameObject) { if (!gameObject) { return {}; } std::vector names; names.reserve(8); const ::XCEngine::Components::GameObject* current = gameObject; while (current) { names.push_back(current->GetName()); current = current->GetParent(); } std::string path; for (auto it = names.rbegin(); it != names.rend(); ++it) { if (!path.empty()) { path += "/"; } path += *it; } return path; } inline void CollectAssetCandidatesRecursive( const AssetItemPtr& root, const std::string& projectPath, const ReferencePickerOptions& options, std::vector& outCandidates) { if (!root) { return; } for (const AssetItemPtr& child : root->children) { if (!child) { continue; } if (child->isFolder) { CollectAssetCandidatesRecursive(child, projectPath, options, outCandidates); continue; } if (!MatchesSupportedAssetExtension(child->extensionLower, options.supportedAssetExtensions)) { continue; } ReferencePickerCandidate candidate; candidate.label = GetAssetDisplayName(child); candidate.secondaryLabel = ProjectFileUtils::MakeProjectRelativePath(projectPath, child->fullPath); candidate.normalizedSearchText = NormalizeSearchText(candidate.label + " " + candidate.secondaryLabel); candidate.iconKind = ResolveAssetIconKind(child); candidate.assetPath = std::move(candidate.secondaryLabel); candidate.secondaryLabel = candidate.assetPath; outCandidates.push_back(std::move(candidate)); } } inline void CollectSceneCandidatesRecursive( ::XCEngine::Components::GameObject* gameObject, const ReferencePickerOptions& options, std::vector& outCandidates, int depth = 0) { if (!gameObject) { return; } if (options.sceneFilter && options.sceneFilter(*gameObject)) { ReferencePickerCandidate candidate; candidate.label = gameObject->GetName(); candidate.secondaryLabel = BuildSceneHierarchyPath(gameObject); candidate.normalizedSearchText = NormalizeSearchText(candidate.label + " " + candidate.secondaryLabel); candidate.iconKind = AssetIconKind::GameObject; candidate.sceneObjectId = gameObject->GetID(); candidate.indent = static_cast(depth) * 14.0f; outCandidates.push_back(std::move(candidate)); } for (::XCEngine::Components::GameObject* child : gameObject->GetChildren()) { CollectSceneCandidatesRecursive(child, options, outCandidates, depth + 1); } } inline bool MatchesSearch(const ReferencePickerCandidate& candidate, const SearchQuery& query) { return query.Empty() || query.MatchesNormalized(candidate.normalizedSearchText); } inline bool DrawPickerTabButton(const char* label, bool active, ImVec2 size) { const ImVec4 buttonColor = active ? ReferencePickerItemActiveColor() : ReferencePickerItemColor(); const ImVec4 hoverColor = ReferencePickerItemHoveredColor(); const ImVec4 activeColor = ReferencePickerItemActiveColor(); ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hoverColor); ImGui::PushStyleColor(ImGuiCol_ButtonActive, activeColor); const bool pressed = ImGui::Button(label, size); ImGui::PopStyleColor(3); return pressed; } inline bool DrawPickerSearchField(char* buffer, size_t bufferSize, const char* hint) { ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, SearchFieldFramePadding()); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, SearchFieldFrameRounding()); ImGui::PushStyleColor(ImGuiCol_FrameBg, ReferencePickerSurfaceColor()); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ReferencePickerItemHoveredColor()); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ReferencePickerItemActiveColor()); ImGui::SetNextItemWidth(-1.0f); const bool changed = ImGui::InputTextWithHint("##ReferencePickerSearch", hint, buffer, bufferSize); const ImVec2 min = ImGui::GetItemRectMin(); const ImVec2 max = ImGui::GetItemRectMax(); const ImVec2 center(min.x + SearchFieldFramePadding().x * 0.5f, (min.y + max.y) * 0.5f); ImDrawList* drawList = ImGui::GetWindowDrawList(); const ImU32 glyphColor = ImGui::GetColorU32(ReferencePickerTextDisabledColor()); drawList->AddCircle(center, 4.0f, glyphColor, 16, 1.5f); drawList->AddLine( ImVec2(center.x + 3.0f, center.y + 3.0f), ImVec2(center.x + 7.0f, center.y + 7.0f), glyphColor, 1.5f); ImGui::PopStyleColor(3); ImGui::PopStyleVar(2); return changed; } inline bool DrawCurrentReferenceField( const char* displayText, const char* hint, AssetIconKind iconKind, float width, bool hasValue, bool* hoveredOut = nullptr) { const float frameHeight = ImGui::GetFrameHeight(); const ImVec2 size((std::max)(width, 1.0f), frameHeight); ImGui::InvisibleButton("##ReferencePickerField", size); const bool hovered = ImGui::IsItemHovered(); const bool held = ImGui::IsItemActive(); if (hoveredOut) { *hoveredOut = hovered; } const ImVec2 min = ImGui::GetItemRectMin(); const ImVec2 max = ImGui::GetItemRectMax(); ImDrawList* drawList = ImGui::GetWindowDrawList(); drawList->AddRectFilled( min, max, ImGui::GetColorU32( held ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), ImGui::GetStyle().FrameRounding); drawList->AddRect( min, max, ImGui::GetColorU32(ImGuiCol_Border), ImGui::GetStyle().FrameRounding); const float iconSize = 16.0f; const float iconMinX = min.x + 6.0f; const float iconMinY = min.y + (frameHeight - iconSize) * 0.5f; DrawAssetIcon( drawList, ImVec2(iconMinX, iconMinY), ImVec2(iconMinX + iconSize, iconMinY + iconSize), iconKind); const char* text = hasValue ? displayText : hint; const ImU32 textColor = ImGui::GetColorU32(hasValue ? ImGuiCol_Text : ImGuiCol_TextDisabled); const ImVec2 textSize = ImGui::CalcTextSize(text ? text : ""); const float textX = iconMinX + iconSize + 6.0f; const float textY = min.y + (frameHeight - textSize.y) * 0.5f; drawList->PushClipRect( ImVec2(textX, min.y), ImVec2(max.x - 6.0f, max.y), true); drawList->AddText(ImVec2(textX, textY), textColor, text ? text : ""); drawList->PopClipRect(); return ImGui::IsItemClicked(ImGuiMouseButton_Left); } inline bool DrawCandidateRow( const char* id, const char* label, const char* secondaryLabel, AssetIconKind iconKind, bool selected, float indent = 0.0f) { const bool hasSecondary = secondaryLabel != nullptr && secondaryLabel[0] != '\0'; const float rowHeight = hasSecondary ? ImGui::GetTextLineHeight() * 2.0f + 10.0f : ImGui::GetFrameHeight(); const ImVec2 rowSize((std::max)(ImGui::GetContentRegionAvail().x, 1.0f), rowHeight); ImGui::InvisibleButton(id, rowSize); const bool hovered = ImGui::IsItemHovered(); const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); const ImVec2 min = ImGui::GetItemRectMin(); const ImVec2 max = ImGui::GetItemRectMax(); ImDrawList* drawList = ImGui::GetWindowDrawList(); if (selected || hovered) { drawList->AddRectFilled( min, max, ImGui::GetColorU32(selected ? ReferencePickerItemActiveColor() : ReferencePickerItemHoveredColor()), 3.0f); } const float iconSize = 16.0f; const float iconMinX = min.x + 8.0f + indent; const float iconMinY = min.y + (rowHeight - iconSize) * 0.5f; DrawAssetIcon( drawList, ImVec2(iconMinX, iconMinY), ImVec2(iconMinX + iconSize, iconMinY + iconSize), iconKind); const float textX = iconMinX + iconSize + 8.0f; const float primaryY = min.y + (hasSecondary ? 3.0f : (rowHeight - ImGui::GetTextLineHeight()) * 0.5f); const float secondaryY = primaryY + ImGui::GetTextLineHeight(); drawList->PushClipRect(ImVec2(textX, min.y), ImVec2(max.x - 6.0f, max.y), true); drawList->AddText(ImVec2(textX, primaryY), ImGui::GetColorU32(ReferencePickerTextColor()), label ? label : ""); if (hasSecondary) { drawList->AddText( ImVec2(textX, secondaryY), ImGui::GetColorU32(ReferencePickerTextDisabledColor()), secondaryLabel); } drawList->PopClipRect(); return clicked; } inline bool DrawNoneRow(bool selected) { const ImVec2 rowSize((std::max)(ImGui::GetContentRegionAvail().x, 1.0f), ImGui::GetFrameHeight()); ImGui::InvisibleButton("##ReferencePickerNone", rowSize); const bool hovered = ImGui::IsItemHovered(); const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); const ImVec2 min = ImGui::GetItemRectMin(); const ImVec2 max = ImGui::GetItemRectMax(); if (selected || hovered) { ImGui::GetWindowDrawList()->AddRectFilled( min, max, ImGui::GetColorU32(selected ? ReferencePickerItemActiveColor() : ReferencePickerItemHoveredColor()), 3.0f); } const ImVec2 textSize = ImGui::CalcTextSize("None"); const float textY = min.y + (rowSize.y - textSize.y) * 0.5f; ImGui::GetWindowDrawList()->AddText( ImVec2(min.x + 8.0f, textY), ImGui::GetColorU32(ReferencePickerTextDisabledColor()), "None"); return clicked; } inline void EnsureValidActiveTab(ReferencePickerState& state, const ReferencePickerOptions& options) { if (state.activeTab == ReferencePickerTab::Assets && options.showAssetsTab) { return; } if (state.activeTab == ReferencePickerTab::Scene && options.showSceneTab) { return; } state.activeTab = options.showAssetsTab ? ReferencePickerTab::Assets : ReferencePickerTab::Scene; } inline ReferencePickerTab ResolveDefaultTab( const ReferencePickerOptions& options, const std::string& currentAssetPath, ::XCEngine::Components::GameObject::ID currentSceneObjectId) { if (!currentAssetPath.empty() && options.showAssetsTab) { return ReferencePickerTab::Assets; } if (currentSceneObjectId != ::XCEngine::Components::GameObject::INVALID_ID && options.showSceneTab) { return ReferencePickerTab::Scene; } return options.showAssetsTab ? ReferencePickerTab::Assets : ReferencePickerTab::Scene; } } // namespace Detail inline ReferencePickerInteraction DrawReferencePickerControl( const std::string& currentAssetPath, ::XCEngine::Components::GameObject::ID currentSceneObjectId, const ReferencePickerOptions& options, float width = -1.0f) { ReferencePickerInteraction interaction; constexpr const char* kProjectAssetPayloadType = "ASSET_ITEM"; ImGuiID pickerStateId = ImGui::GetID("##ReferencePickerState"); Detail::ReferencePickerState& state = Detail::GetReferencePickerState(pickerStateId); Detail::EnsureValidActiveTab(state, options); const float resolvedWidth = width > 0.0f ? width : ImGui::GetContentRegionAvail().x; const float fieldWidth = (std::max)(resolvedWidth, 1.0f); const bool hasAssetValue = !currentAssetPath.empty(); const bool hasSceneValue = currentSceneObjectId != ::XCEngine::Components::GameObject::INVALID_ID; std::string currentDisplayText; std::string currentTooltip; AssetIconKind currentIconKind = AssetIconKind::File; if (hasAssetValue) { currentDisplayText = Detail::GetAssetDisplayNameFromPath(currentAssetPath); currentTooltip = currentAssetPath; currentIconKind = Detail::ResolveAssetIconKindFromPath(currentAssetPath); } else if (hasSceneValue) { auto& editorContext = Application::Get().GetEditorContext(); if (::XCEngine::Components::GameObject* gameObject = editorContext.GetSceneManager().GetEntity(currentSceneObjectId)) { currentDisplayText = gameObject->GetName(); currentTooltip = Detail::BuildSceneHierarchyPath(gameObject); } else { currentDisplayText = "Missing"; } currentIconKind = AssetIconKind::GameObject; } bool fieldHovered = false; const bool fieldClicked = Detail::DrawCurrentReferenceField( currentDisplayText.c_str(), options.emptyHint, currentIconKind, fieldWidth, hasAssetValue || hasSceneValue, &fieldHovered); const ImVec2 fieldMin = ImGui::GetItemRectMin(); const ImVec2 fieldMax = ImGui::GetItemRectMax(); if (fieldHovered && !currentTooltip.empty()) { ImGui::SetTooltip("%s", currentTooltip.c_str()); } if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload( kProjectAssetPayloadType, ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) { if (payload->Data != nullptr) { const std::string droppedPath(static_cast(payload->Data)); std::string extensionLower = std::filesystem::path(droppedPath).extension().string(); std::transform(extensionLower.begin(), extensionLower.end(), extensionLower.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); if (Detail::MatchesSupportedAssetExtension(extensionLower, options.supportedAssetExtensions)) { const auto& editorContext = Application::Get().GetEditorContext(); interaction.assignedAssetPath = editorContext.GetProjectPath().empty() ? droppedPath : ProjectFileUtils::MakeProjectRelativePath(editorContext.GetProjectPath(), droppedPath); } } } ImGui::EndDragDropTarget(); } const char* popupId = "##ReferencePickerPopup"; if (fieldClicked) { state.activeTab = Detail::ResolveDefaultTab(options, currentAssetPath, currentSceneObjectId); state.searchBuffer[0] = '\0'; ImGui::OpenPopup(popupId); } ImGui::SetNextWindowPos(ImVec2(fieldMin.x, fieldMax.y + 2.0f), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2((std::max)(fieldWidth, 320.0f), 360.0f), ImGuiCond_Appearing); if (!Detail::BeginReferencePickerPopup(popupId, options.popupTitle)) { return interaction; } Detail::EnsureValidActiveTab(state, options); const bool showTabs = options.showAssetsTab && options.showSceneTab; if (showTabs) { const float availableWidth = ImGui::GetContentRegionAvail().x; const float tabWidth = (availableWidth - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (Detail::DrawPickerTabButton( options.assetsTabLabel, state.activeTab == ReferencePickerTab::Assets, ImVec2(tabWidth, 0.0f))) { state.activeTab = ReferencePickerTab::Assets; } ImGui::SameLine(); if (Detail::DrawPickerTabButton( options.sceneTabLabel, state.activeTab == ReferencePickerTab::Scene, ImVec2(tabWidth, 0.0f))) { state.activeTab = ReferencePickerTab::Scene; } ImGui::Spacing(); } Detail::DrawPickerSearchField(state.searchBuffer.data(), state.searchBuffer.size(), options.searchHint); ImGui::Spacing(); std::vector candidates; candidates.reserve(64); auto& editorContext = Application::Get().GetEditorContext(); if (state.activeTab == ReferencePickerTab::Assets) { if (options.showAssetsTab) { Detail::CollectAssetCandidatesRecursive( editorContext.GetProjectManager().GetRootFolder(), editorContext.GetProjectPath(), options, candidates); } } else { if (options.showSceneTab) { const auto& roots = editorContext.GetSceneManager().GetRootEntities(); for (::XCEngine::Components::GameObject* root : roots) { Detail::CollectSceneCandidatesRecursive(root, options, candidates); } } } const SearchQuery searchQuery(state.searchBuffer.data()); ImGui::BeginChild("##ReferencePickerList", ImVec2(0.0f, 0.0f), true); const bool noneSelected = !hasAssetValue && !hasSceneValue; if (Detail::DrawNoneRow(noneSelected)) { interaction.clearRequested = true; ImGui::CloseCurrentPopup(); } bool anyVisibleCandidate = false; for (size_t index = 0; index < candidates.size(); ++index) { const Detail::ReferencePickerCandidate& candidate = candidates[index]; if (!Detail::MatchesSearch(candidate, searchQuery)) { continue; } anyVisibleCandidate = true; ImGui::PushID(static_cast(index)); const bool selected = candidate.IsAsset() ? candidate.assetPath == currentAssetPath : candidate.sceneObjectId == currentSceneObjectId; const bool clicked = Detail::DrawCandidateRow( "##ReferencePickerCandidate", candidate.label.c_str(), candidate.secondaryLabel.c_str(), candidate.iconKind, selected, candidate.indent); ImGui::PopID(); if (!clicked) { continue; } if (candidate.IsAsset()) { interaction.assignedAssetPath = candidate.assetPath; } else if (candidate.IsSceneObject()) { interaction.assignedSceneObjectId = candidate.sceneObjectId; } ImGui::CloseCurrentPopup(); } if (!anyVisibleCandidate) { ImGui::TextColored( Detail::ReferencePickerTextDisabledColor(), "%s", state.activeTab == ReferencePickerTab::Assets ? options.noAssetsText : options.noSceneText); } ImGui::EndChild(); Detail::EndReferencePickerPopup(); return interaction; } } // namespace UI } // namespace Editor } // namespace XCEngine