Unify panel search behavior and polish console UI
This commit is contained in:
@@ -1,15 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Application.h"
|
||||
#include "Actions/ProjectActionRouter.h"
|
||||
#include "UI/UI.h"
|
||||
#include "Utils/ProjectFileUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
|
||||
@@ -22,83 +14,35 @@ struct AssetReferenceInteraction {
|
||||
bool clearRequested = false;
|
||||
};
|
||||
|
||||
inline std::string ToProjectRelativeAssetPath(const std::string& assetPath) {
|
||||
if (assetPath.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::string& projectPath = Application::Get().GetEditorContext().GetProjectPath();
|
||||
if (projectPath.empty()) {
|
||||
return assetPath;
|
||||
}
|
||||
|
||||
return ProjectFileUtils::MakeProjectRelativePath(projectPath, assetPath);
|
||||
}
|
||||
|
||||
inline bool HasSupportedExtension(
|
||||
const std::string& path,
|
||||
std::initializer_list<const char*> supportedExtensions) {
|
||||
std::string extension = std::filesystem::path(path).extension().string();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
|
||||
for (const char* supportedExtension : supportedExtensions) {
|
||||
if (supportedExtension != nullptr && extension == supportedExtension) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
inline AssetReferenceInteraction DrawAssetReferenceProperty(
|
||||
const char* label,
|
||||
const std::string& currentPath,
|
||||
const char* emptyHint,
|
||||
std::initializer_list<const char*> supportedExtensions) {
|
||||
AssetReferenceInteraction interaction;
|
||||
const std::string popupTitle = std::string("Select ") + (label ? label : "Object");
|
||||
UI::ReferencePickerOptions pickerOptions;
|
||||
pickerOptions.popupTitle = popupTitle.c_str();
|
||||
pickerOptions.emptyHint = emptyHint;
|
||||
pickerOptions.searchHint = "Search";
|
||||
pickerOptions.noAssetsText = "No compatible assets.";
|
||||
pickerOptions.noSceneText = "No compatible scene objects.";
|
||||
pickerOptions.assetsTabLabel = "Assets";
|
||||
pickerOptions.sceneTabLabel = "Scene";
|
||||
pickerOptions.showAssetsTab = true;
|
||||
pickerOptions.showSceneTab = true;
|
||||
pickerOptions.supportedAssetExtensions = supportedExtensions;
|
||||
|
||||
UI::DrawPropertyRow(label, UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) {
|
||||
constexpr float kClearButtonWidth = 52.0f;
|
||||
|
||||
std::array<char, 512> buffer{};
|
||||
if (!currentPath.empty()) {
|
||||
strncpy_s(buffer.data(), buffer.size(), currentPath.c_str(), _TRUNCATE);
|
||||
}
|
||||
|
||||
const float spacing = ImGui::GetStyle().ItemSpacing.x;
|
||||
const float fieldWidth = (std::max)(layout.controlWidth - kClearButtonWidth - spacing, 1.0f);
|
||||
ImGui::SetNextItemWidth(fieldWidth);
|
||||
ImGui::InputTextWithHint(
|
||||
"##AssetPath",
|
||||
emptyHint,
|
||||
buffer.data(),
|
||||
buffer.size(),
|
||||
ImGuiInputTextFlags_ReadOnly);
|
||||
|
||||
if (ImGui::IsItemHovered() && !currentPath.empty()) {
|
||||
ImGui::SetTooltip("%s", currentPath.c_str());
|
||||
}
|
||||
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(
|
||||
Actions::ProjectAssetPayloadType(),
|
||||
ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) {
|
||||
if (payload->Data != nullptr) {
|
||||
const std::string droppedPath(static_cast<const char*>(payload->Data));
|
||||
if (HasSupportedExtension(droppedPath, supportedExtensions)) {
|
||||
interaction.assignedPath = ToProjectRelativeAssetPath(droppedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
ImGui::SameLine(0.0f, spacing);
|
||||
ImGui::BeginDisabled(currentPath.empty());
|
||||
interaction.clearRequested = UI::InspectorActionButton("Clear", ImVec2(kClearButtonWidth, 0.0f));
|
||||
ImGui::EndDisabled();
|
||||
UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
|
||||
const UI::ReferencePickerInteraction pickerInteraction =
|
||||
UI::DrawReferencePickerControl(
|
||||
currentPath,
|
||||
::XCEngine::Components::GameObject::INVALID_ID,
|
||||
pickerOptions,
|
||||
layout.controlWidth);
|
||||
interaction.assignedPath = pickerInteraction.assignedAssetPath;
|
||||
interaction.clearRequested = pickerInteraction.clearRequested;
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
722
editor/src/UI/ReferencePicker.h
Normal file
722
editor/src/UI/ReferencePicker.h
Normal file
@@ -0,0 +1,722 @@
|
||||
#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 <XCEngine/Components/GameObject.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<const char*> supportedAssetExtensions;
|
||||
std::function<bool(const ::XCEngine::Components::GameObject&)> 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<char, 128> 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<ImGuiID, ReferencePickerState> states;
|
||||
return states[id];
|
||||
}
|
||||
|
||||
inline bool MatchesSupportedAssetExtension(
|
||||
std::string_view extensionLower,
|
||||
std::initializer_list<const char*> 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<char>(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<std::string> 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<ReferencePickerCandidate>& 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<ReferencePickerCandidate>& 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<float>(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<const char*>(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<char>(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<Detail::ReferencePickerCandidate> 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<int>(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
|
||||
50
editor/src/UI/SearchText.h
Normal file
50
editor/src/UI/SearchText.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace UI {
|
||||
|
||||
inline std::string NormalizeSearchText(std::string_view text) {
|
||||
std::string normalized(text);
|
||||
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
class SearchQuery {
|
||||
public:
|
||||
SearchQuery() = default;
|
||||
|
||||
explicit SearchQuery(std::string_view text)
|
||||
: m_text(NormalizeSearchText(text)) {
|
||||
}
|
||||
|
||||
bool Empty() const {
|
||||
return m_text.empty();
|
||||
}
|
||||
|
||||
const std::string& Text() const {
|
||||
return m_text;
|
||||
}
|
||||
|
||||
bool Matches(std::string_view text) const {
|
||||
return Empty() || NormalizeSearchText(text).find(m_text) != std::string::npos;
|
||||
}
|
||||
|
||||
bool MatchesNormalized(std::string_view normalizedText) const {
|
||||
return Empty() || normalizedText.find(m_text) != std::string::npos;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_text;
|
||||
};
|
||||
|
||||
} // namespace UI
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -233,7 +233,7 @@ inline ImVec2 ProjectNavigationPanePadding() {
|
||||
}
|
||||
|
||||
inline ImVec2 ProjectBrowserPanePadding() {
|
||||
return ImVec2(10.0f, 8.0f);
|
||||
return ImVec2(16.0f, 12.0f);
|
||||
}
|
||||
|
||||
inline ImVec2 NavigationTreeNodeFramePadding() {
|
||||
@@ -269,7 +269,11 @@ inline float NavigationTreePrefixLabelGap() {
|
||||
}
|
||||
|
||||
inline float DisclosureArrowScale() {
|
||||
return 0.18f;
|
||||
return 0.20f;
|
||||
}
|
||||
|
||||
inline ImVec4 DisclosureArrowColor() {
|
||||
return ImVec4(0.42f, 0.42f, 0.42f, 1.0f);
|
||||
}
|
||||
|
||||
inline ImVec4 NavigationTreePrefixColor(bool selected = false, bool hovered = false) {
|
||||
@@ -339,13 +343,17 @@ inline ImVec4 ToolbarBackgroundColor() {
|
||||
}
|
||||
|
||||
inline ImVec2 SearchFieldFramePadding() {
|
||||
return ImVec2(7.0f, 4.0f);
|
||||
return ImVec2(22.0f, 1.0f);
|
||||
}
|
||||
|
||||
inline float SearchFieldFrameRounding() {
|
||||
return 2.0f;
|
||||
}
|
||||
|
||||
inline float SearchFieldVerticalOffset() {
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
inline ImVec2 InlineRenameFieldFramePadding() {
|
||||
return ImVec2(4.0f, 0.0f);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include "PopupState.h"
|
||||
#include "PropertyLayout.h"
|
||||
#include "PropertyGrid.h"
|
||||
#include "ReferencePicker.h"
|
||||
#include "SearchText.h"
|
||||
#include "SplitterChrome.h"
|
||||
#include "ScalarControls.h"
|
||||
#include "SceneStatusWidget.h"
|
||||
|
||||
@@ -192,11 +192,35 @@ inline bool ToolbarSearchField(
|
||||
char* buffer,
|
||||
size_t bufferSize,
|
||||
float trailingWidth = 0.0f) {
|
||||
const float originalCursorY = ImGui::GetCursorPosY();
|
||||
const float verticalOffset = SearchFieldVerticalOffset();
|
||||
if (verticalOffset != 0.0f) {
|
||||
const float nextCursorY = originalCursorY + verticalOffset;
|
||||
ImGui::SetCursorPosY(nextCursorY > 0.0f ? nextCursorY : 0.0f);
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, SearchFieldFramePadding());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, SearchFieldFrameRounding());
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, ToolbarButtonColor(false));
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ToolbarButtonHoveredColor(false));
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ToolbarButtonActiveColor());
|
||||
const float width = ImGui::GetContentRegionAvail().x - trailingWidth;
|
||||
ImGui::SetNextItemWidth(width > 0.0f ? width : 0.0f);
|
||||
const bool changed = ImGui::InputTextWithHint(id, 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(ConsoleSecondaryTextColor());
|
||||
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;
|
||||
}
|
||||
@@ -514,7 +538,7 @@ inline ComponentSectionResult BeginComponentSection(
|
||||
style.FrameBorderSize);
|
||||
}
|
||||
|
||||
DrawDisclosureArrow(drawList, arrowRect.Min, arrowRect.Max, open, ImGui::GetColorU32(ImGuiCol_Text));
|
||||
DrawDisclosureArrow(drawList, arrowRect.Min, arrowRect.Max, open, ImGui::GetColorU32(DisclosureArrowColor()));
|
||||
|
||||
if (label && label[0] != '\0') {
|
||||
const float textX = itemMin.x + arrowSlotWidth;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <imgui.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
@@ -40,6 +39,8 @@ struct ConsoleSeverityCounts {
|
||||
size_t errorCount = 0;
|
||||
};
|
||||
|
||||
bool CanOpenSourceLocation(const LogEntry& entry);
|
||||
|
||||
struct ConsoleRowData {
|
||||
uint64_t serial = 0;
|
||||
LogEntry entry = {};
|
||||
@@ -143,13 +144,6 @@ bool IsErrorLevel(LogLevel level) {
|
||||
return level == LogLevel::Error || level == LogLevel::Fatal;
|
||||
}
|
||||
|
||||
std::string ToLowerCopy(std::string text) {
|
||||
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string BuildEntryKey(const LogEntry& entry) {
|
||||
std::string key;
|
||||
key.reserve(128 + entry.message.Length() + entry.file.Length() + entry.function.Length());
|
||||
@@ -173,15 +167,11 @@ std::string BuildSearchHaystack(const LogEntry& entry) {
|
||||
haystack += entry.file.CStr();
|
||||
haystack.push_back('\n');
|
||||
haystack += entry.function.CStr();
|
||||
return ToLowerCopy(std::move(haystack));
|
||||
return haystack;
|
||||
}
|
||||
|
||||
bool MatchesSearch(const LogEntry& entry, const std::string& searchText) {
|
||||
if (searchText.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return BuildSearchHaystack(entry).find(searchText) != std::string::npos;
|
||||
bool MatchesSearch(const LogEntry& entry, const XCEngine::Editor::UI::SearchQuery& searchQuery) {
|
||||
return searchQuery.Matches(BuildSearchHaystack(entry));
|
||||
}
|
||||
|
||||
void CountSeverity(ConsoleSeverityCounts& counts, LogLevel level) {
|
||||
@@ -210,7 +200,7 @@ uint64_t FindLatestSerial(const std::vector<EditorConsoleRecord>& records) {
|
||||
std::vector<ConsoleRowData> BuildVisibleRows(
|
||||
const std::vector<EditorConsoleRecord>& records,
|
||||
const XCEngine::Editor::UI::ConsoleFilterState& filterState,
|
||||
const std::string& searchText,
|
||||
const XCEngine::Editor::UI::SearchQuery& searchQuery,
|
||||
ConsoleSeverityCounts& counts) {
|
||||
std::vector<ConsoleRowData> rows;
|
||||
rows.reserve(records.size());
|
||||
@@ -218,7 +208,7 @@ std::vector<ConsoleRowData> BuildVisibleRows(
|
||||
if (!filterState.Collapse()) {
|
||||
for (const EditorConsoleRecord& record : records) {
|
||||
CountSeverity(counts, record.entry.level);
|
||||
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchText)) {
|
||||
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchQuery)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -236,7 +226,7 @@ std::vector<ConsoleRowData> BuildVisibleRows(
|
||||
rowIndicesByKey.reserve(records.size());
|
||||
for (const EditorConsoleRecord& record : records) {
|
||||
CountSeverity(counts, record.entry.level);
|
||||
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchText)) {
|
||||
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchQuery)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -358,15 +348,6 @@ void DrawSeverityIcon(
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSearchGlyph(ImDrawList* drawList, const ImVec2& center, ImU32 color) {
|
||||
drawList->AddCircle(center, 4.0f, color, 16, 1.5f);
|
||||
drawList->AddLine(
|
||||
ImVec2(center.x + 3.0f, center.y + 3.0f),
|
||||
ImVec2(center.x + 7.0f, center.y + 7.0f),
|
||||
color,
|
||||
1.5f);
|
||||
}
|
||||
|
||||
bool DrawToolbarDropdownButton(
|
||||
const char* id,
|
||||
const char* label,
|
||||
@@ -525,6 +506,37 @@ bool DrawToolbarButton(
|
||||
return pressed;
|
||||
}
|
||||
|
||||
bool DrawCompactCheckedMenuItem(const char* label, bool checked) {
|
||||
if (!label) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool clicked = ImGui::Selectable(label, false, ImGuiSelectableFlags_SpanAvailWidth);
|
||||
if (checked) {
|
||||
const ImRect rect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
const ImU32 color = ImGui::GetColorU32(ImGuiCol_CheckMark);
|
||||
const float height = rect.Max.y - rect.Min.y;
|
||||
const float checkWidth = height * 0.28f;
|
||||
const float checkHeight = height * 0.18f;
|
||||
const float x = rect.Max.x - 12.0f;
|
||||
const float y = rect.Min.y + height * 0.52f;
|
||||
|
||||
drawList->AddLine(
|
||||
ImVec2(x - checkWidth, y - checkHeight * 0.15f),
|
||||
ImVec2(x - checkWidth * 0.42f, y + checkHeight),
|
||||
color,
|
||||
1.4f);
|
||||
drawList->AddLine(
|
||||
ImVec2(x - checkWidth * 0.42f, y + checkHeight),
|
||||
ImVec2(x + checkWidth, y - checkHeight),
|
||||
color,
|
||||
1.4f);
|
||||
}
|
||||
|
||||
return clicked;
|
||||
}
|
||||
|
||||
void DrawToolbarArrowDropdownButton(
|
||||
const char* id,
|
||||
float width,
|
||||
@@ -566,28 +578,6 @@ void DrawToolbarArrowDropdownButton(
|
||||
}
|
||||
}
|
||||
|
||||
bool DrawConsoleSearchField(const char* id, char* buffer, size_t bufferSize) {
|
||||
const float originalCursorY = ImGui::GetCursorPosY();
|
||||
ImGui::SetCursorPosY((std::max)(0.0f, originalCursorY - 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(22.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 2.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, XCEngine::Editor::UI::ToolbarButtonColor(false));
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, XCEngine::Editor::UI::ToolbarButtonHoveredColor(false));
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, XCEngine::Editor::UI::ToolbarButtonActiveColor());
|
||||
ImGui::SetNextItemWidth((std::max)(0.0f, ImGui::GetContentRegionAvail().x));
|
||||
const bool changed = ImGui::InputTextWithHint(id, "Search", buffer, bufferSize);
|
||||
const ImVec2 min = ImGui::GetItemRectMin();
|
||||
const ImVec2 max = ImGui::GetItemRectMax();
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
DrawSearchGlyph(
|
||||
drawList,
|
||||
ImVec2(min.x + 11.0f, (min.y + max.y) * 0.5f),
|
||||
ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleSecondaryTextColor()));
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::PopStyleVar(2);
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool DrawSeverityToggleButton(
|
||||
const char* id,
|
||||
ConsoleSeverityVisual severity,
|
||||
@@ -595,14 +585,14 @@ bool DrawSeverityToggleButton(
|
||||
bool& active,
|
||||
const char* tooltip) {
|
||||
const float originalCursorY = ImGui::GetCursorPosY();
|
||||
ImGui::SetCursorPosY((std::max)(0.0f, originalCursorY - 1.0f));
|
||||
ImGui::SetCursorPosY((std::max)(0.0f, originalCursorY - kConsoleToolbarRowPaddingY));
|
||||
|
||||
const std::string countText = std::to_string(count);
|
||||
const ImVec2 countSize = ImGui::CalcTextSize(countText.c_str());
|
||||
const ImVec2 padding(6.0f, 3.0f);
|
||||
const float iconRadius = 6.0f;
|
||||
const float gap = 4.0f;
|
||||
const float buttonHeight = kConsoleToolbarButtonHeight + 2.0f;
|
||||
const ImVec2 padding(7.0f, 3.0f);
|
||||
const float iconRadius = 10.0f;
|
||||
const float gap = 5.0f;
|
||||
const float buttonHeight = kConsoleToolbarHeight;
|
||||
const ImVec2 size(
|
||||
(std::max)(
|
||||
kConsoleCounterWidth,
|
||||
@@ -643,9 +633,9 @@ bool DrawSeverityToggleButton(
|
||||
float CalculateSeverityToggleButtonWidth(size_t count) {
|
||||
const std::string countText = std::to_string(count);
|
||||
const ImVec2 countSize = ImGui::CalcTextSize(countText.c_str());
|
||||
const ImVec2 padding(6.0f, 3.0f);
|
||||
const float iconRadius = 6.0f;
|
||||
const float gap = 4.0f;
|
||||
const ImVec2 padding(7.0f, 3.0f);
|
||||
const float iconRadius = 10.0f;
|
||||
const float gap = 5.0f;
|
||||
return (std::max)(
|
||||
kConsoleCounterWidth,
|
||||
padding.x * 2.0f + iconRadius * 2.0f + gap + countSize.x);
|
||||
@@ -991,9 +981,9 @@ void ConsolePanel::Render() {
|
||||
m_lastErrorPauseScanSerial = FindLatestSerial(records);
|
||||
}
|
||||
|
||||
const std::string searchText = ToLowerCopy(std::string(m_searchBuffer));
|
||||
const UI::SearchQuery searchQuery(m_searchBuffer);
|
||||
ConsoleSeverityCounts counts;
|
||||
std::vector<ConsoleRowData> rows = BuildVisibleRows(records, m_filterState, searchText, counts);
|
||||
std::vector<ConsoleRowData> rows = BuildVisibleRows(records, m_filterState, searchQuery, counts);
|
||||
const ConsoleRowData* selectedRow = ResolveSelectedRow(rows, m_selectedSerial, m_selectedEntryKey);
|
||||
if (selectedRow) {
|
||||
m_selectedEntryKey = selectedRow->entryKey;
|
||||
@@ -1058,7 +1048,7 @@ void ConsolePanel::Render() {
|
||||
}
|
||||
ImGui::SameLine(0.0f, 1.0f);
|
||||
DrawToolbarArrowDropdownButton("##ConsoleClearOptions", 16.0f, [&]() {
|
||||
if (ImGui::MenuItem("Clear on Play", nullptr, m_filterState.ClearOnPlay())) {
|
||||
if (DrawCompactCheckedMenuItem("Clear on Play", m_filterState.ClearOnPlay())) {
|
||||
m_filterState.ClearOnPlay() = !m_filterState.ClearOnPlay();
|
||||
}
|
||||
});
|
||||
@@ -1077,7 +1067,7 @@ void ConsolePanel::Render() {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
m_requestSearchFocus = false;
|
||||
}
|
||||
DrawConsoleSearchField("##ConsoleSearch", m_searchBuffer, sizeof(m_searchBuffer));
|
||||
UI::ToolbarSearchField("##ConsoleSearch", "Search", m_searchBuffer, sizeof(m_searchBuffer));
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
DrawSeverityToggleButton("##ConsoleLogFilter", ConsoleSeverityVisual::Log, counts.logCount, m_filterState.ShowLog(), "Log");
|
||||
@@ -1162,8 +1152,8 @@ void ConsolePanel::Render() {
|
||||
|
||||
if (rows.empty()) {
|
||||
UI::DrawEmptyState(
|
||||
searchText.empty() ? "No messages" : "No search results",
|
||||
searchText.empty() ? nullptr : "No console entries match the current search",
|
||||
searchQuery.Empty() ? "No messages" : "No search results",
|
||||
searchQuery.Empty() ? nullptr : "No console entries match the current search",
|
||||
ImVec2(12.0f, 12.0f));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,37 @@
|
||||
#include "Core/EditorEvents.h"
|
||||
#include "Core/EventBus.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kHierarchyToolbarHeight = 26.0f;
|
||||
constexpr float kHierarchyToolbarPaddingY = 3.0f;
|
||||
constexpr float kHierarchySearchWidth = 220.0f;
|
||||
|
||||
bool HierarchyNodeMatchesSearch(
|
||||
XCEngine::Components::GameObject* gameObject,
|
||||
const XCEngine::Editor::UI::SearchQuery& searchQuery) {
|
||||
if (!gameObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (searchQuery.Matches(gameObject->GetName().c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < gameObject->GetChildCount(); ++i) {
|
||||
if (HierarchyNodeMatchesSearch(gameObject->GetChild(i), searchQuery)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void DrawHierarchyTreePrefix(const XCEngine::Editor::UI::TreeNodePrefixContext& context) {
|
||||
if (!context.drawList) {
|
||||
return;
|
||||
@@ -157,6 +184,33 @@ void HierarchyPanel::Render() {
|
||||
|
||||
Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Hierarchy);
|
||||
|
||||
{
|
||||
UI::PanelToolbarScope toolbar(
|
||||
"HierarchyToolbar",
|
||||
kHierarchyToolbarHeight,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse,
|
||||
true,
|
||||
ImVec2(UI::ToolbarPadding().x, kHierarchyToolbarPaddingY),
|
||||
UI::ToolbarItemSpacing(),
|
||||
UI::HierarchyInspectorPanelBackgroundColor());
|
||||
if (toolbar.IsOpen()) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f));
|
||||
if (ImGui::BeginTable("##HierarchyToolbarLayout", 2, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) {
|
||||
ImGui::TableSetupColumn("##Spacer", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, kHierarchySearchWidth);
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
UI::ToolbarSearchField("##HierarchySearch", "Search", m_searchBuffer, sizeof(m_searchBuffer));
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
}
|
||||
|
||||
UI::PanelContentScope content("EntityList", UI::HierarchyPanelContentPadding());
|
||||
if (!content.IsOpen()) {
|
||||
ImGui::PopStyleColor(2);
|
||||
@@ -165,10 +219,18 @@ void HierarchyPanel::Render() {
|
||||
|
||||
auto& sceneManager = m_context->GetSceneManager();
|
||||
auto rootEntities = sceneManager.GetRootEntities();
|
||||
const UI::SearchQuery searchQuery(m_searchBuffer);
|
||||
size_t visibleEntityCount = 0;
|
||||
UI::ResetTreeLayout();
|
||||
|
||||
for (auto* gameObject : rootEntities) {
|
||||
RenderEntity(gameObject);
|
||||
if (RenderEntity(gameObject, searchQuery)) {
|
||||
++visibleEntityCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (!searchQuery.Empty() && visibleEntityCount == 0) {
|
||||
UI::DrawEmptyState("No matching entities", "Try a different search term");
|
||||
}
|
||||
|
||||
Actions::DrawHierarchyBackgroundInteraction(*m_context, m_renameState);
|
||||
@@ -179,7 +241,7 @@ void HierarchyPanel::Render() {
|
||||
Actions::TraceHierarchyPopup("Hierarchy background popup opened via background surface");
|
||||
s_backgroundContextOpen = true;
|
||||
}
|
||||
Actions::DrawHierarchyCreateActions(*m_context, nullptr);
|
||||
Actions::DrawHierarchyContextActions(*m_context, nullptr, true);
|
||||
UI::EndContextMenu();
|
||||
} else if (s_backgroundContextOpen) {
|
||||
Actions::TraceHierarchyPopup("Hierarchy background popup closed");
|
||||
@@ -189,18 +251,32 @@ void HierarchyPanel::Render() {
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject) {
|
||||
if (!gameObject) return;
|
||||
bool HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject, const UI::SearchQuery& searchQuery) {
|
||||
if (!gameObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool visibleInSearch = HierarchyNodeMatchesSearch(gameObject, searchQuery);
|
||||
if (!visibleInSearch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool searching = !searchQuery.Empty();
|
||||
|
||||
ImGui::PushID(static_cast<int>(gameObject->GetID()));
|
||||
|
||||
UI::TreeNodeDefinition nodeDefinition =
|
||||
BuildHierarchyNodeDefinition(*m_context, gameObject, m_itemContextMenu);
|
||||
const std::string persistenceKey = std::to_string(gameObject->GetUUID());
|
||||
nodeDefinition.persistenceKey = persistenceKey;
|
||||
const std::string persistenceKey = searching ? std::string() : std::to_string(gameObject->GetUUID());
|
||||
if (searching) {
|
||||
nodeDefinition.options.defaultOpen = gameObject->GetChildCount() > 0;
|
||||
nodeDefinition.persistenceKey = {};
|
||||
} else {
|
||||
nodeDefinition.persistenceKey = persistenceKey;
|
||||
}
|
||||
const bool editing = m_renameState.IsEditing(gameObject->GetID());
|
||||
const UI::TreeNodeResult node = UI::DrawTreeNode(
|
||||
&m_treeState,
|
||||
searching ? nullptr : &m_treeState,
|
||||
(void*)gameObject->GetUUID(),
|
||||
editing ? "" : gameObject->GetName().c_str(),
|
||||
nodeDefinition);
|
||||
@@ -220,12 +296,13 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject
|
||||
|
||||
if (node.open) {
|
||||
for (size_t i = 0; i < gameObject->GetChildCount(); i++) {
|
||||
RenderEntity(gameObject->GetChild(i));
|
||||
RenderEntity(gameObject->GetChild(i), searchQuery);
|
||||
}
|
||||
UI::EndTreeNode();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HierarchyPanel::BeginRename(::XCEngine::Components::GameObject* gameObject) {
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
#include "Panel.h"
|
||||
#include "UI/PopupState.h"
|
||||
#include "UI/SearchText.h"
|
||||
#include "UI/TreeView.h"
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include "Core/ISceneManager.h"
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
@@ -20,7 +22,7 @@ public:
|
||||
private:
|
||||
void OnSelectionChanged(const struct SelectionChangedEvent& event);
|
||||
void OnRenameRequested(const struct EntityRenameRequestedEvent& event);
|
||||
void RenderEntity(::XCEngine::Components::GameObject* gameObject);
|
||||
bool RenderEntity(::XCEngine::Components::GameObject* gameObject, const UI::SearchQuery& searchQuery = UI::SearchQuery());
|
||||
void BeginRename(::XCEngine::Components::GameObject* gameObject);
|
||||
void CommitRename();
|
||||
void CancelRename();
|
||||
@@ -31,6 +33,7 @@ private:
|
||||
UI::TargetedPopupState<::XCEngine::Components::GameObject*> m_itemContextMenu;
|
||||
uint64_t m_selectionHandlerId = 0;
|
||||
uint64_t m_renameRequestHandlerId = 0;
|
||||
char m_searchBuffer[256] = "";
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kProjectToolbarHeight = 26.0f;
|
||||
constexpr float kProjectToolbarPaddingY = 3.0f;
|
||||
|
||||
template <typename Fn>
|
||||
void QueueDeferredAction(std::function<void()>& pendingAction, Fn&& fn) {
|
||||
if (!pendingAction) {
|
||||
@@ -234,16 +237,17 @@ void ProjectPanel::Render() {
|
||||
void ProjectPanel::RenderToolbar() {
|
||||
UI::PanelToolbarScope toolbar(
|
||||
"ProjectToolbar",
|
||||
UI::ProjectPanelToolbarHeight(),
|
||||
kProjectToolbarHeight,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse,
|
||||
true,
|
||||
UI::ToolbarPadding(),
|
||||
ImVec2(UI::ToolbarPadding().x, kProjectToolbarPaddingY),
|
||||
UI::ToolbarItemSpacing(),
|
||||
UI::ProjectPanelToolbarBackgroundColor());
|
||||
if (!toolbar.IsOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f));
|
||||
if (ImGui::BeginTable("##ProjectToolbarLayout", 2, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) {
|
||||
ImGui::TableSetupColumn("##Spacer", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 220.0f);
|
||||
@@ -257,6 +261,7 @@ void ProjectPanel::RenderToolbar() {
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
|
||||
@@ -359,13 +364,13 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
|
||||
std::vector<AssetItemPtr> visibleItems;
|
||||
const auto& items = manager.GetCurrentItems();
|
||||
const std::string search = m_searchBuffer;
|
||||
const UI::SearchQuery searchQuery(m_searchBuffer);
|
||||
if (m_renameState.IsActive() && manager.FindCurrentItemIndex(m_renameState.Item()) < 0) {
|
||||
CancelRename();
|
||||
}
|
||||
visibleItems.reserve(items.size());
|
||||
for (const auto& item : items) {
|
||||
if ((m_renameState.IsActive() && item && item->fullPath == m_renameState.Item()) || MatchesSearch(item, search)) {
|
||||
if ((m_renameState.IsActive() && item && item->fullPath == m_renameState.Item()) || MatchesSearch(item, searchQuery)) {
|
||||
visibleItems.push_back(item);
|
||||
}
|
||||
}
|
||||
@@ -375,7 +380,10 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectBrowserPanePadding());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::AssetGridSpacing());
|
||||
const bool bodyOpen = ImGui::BeginChild("ProjectBrowserBody", ImVec2(0.0f, 0.0f), false);
|
||||
const bool bodyOpen = ImGui::BeginChild(
|
||||
"ProjectBrowserBody",
|
||||
ImVec2(0.0f, 0.0f),
|
||||
ImGuiChildFlags_AlwaysUseWindowPadding);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
if (!bodyOpen) {
|
||||
@@ -424,7 +432,7 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing);
|
||||
}
|
||||
|
||||
if (visibleItems.empty() && !search.empty()) {
|
||||
if (visibleItems.empty() && !searchQuery.Empty()) {
|
||||
UI::DrawEmptyState(
|
||||
"No Search Results",
|
||||
"No assets match the current search");
|
||||
@@ -521,7 +529,12 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem
|
||||
isDraggingThisItem,
|
||||
[&](ImDrawList* drawList, const ImVec2& iconMin, const ImVec2& iconMax) {
|
||||
if (item && item->canUseImagePreview &&
|
||||
UI::DrawTextureAssetPreview(drawList, iconMin, iconMax, item->fullPath)) {
|
||||
UI::DrawTextureAssetPreview(
|
||||
drawList,
|
||||
iconMin,
|
||||
iconMax,
|
||||
item->fullPath,
|
||||
m_context ? m_context->GetProjectPath() : std::string())) {
|
||||
return;
|
||||
}
|
||||
UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind);
|
||||
@@ -584,23 +597,15 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem
|
||||
return interaction;
|
||||
}
|
||||
|
||||
bool ProjectPanel::MatchesSearch(const AssetItemPtr& item, const std::string& search) {
|
||||
bool ProjectPanel::MatchesSearch(const AssetItemPtr& item, const UI::SearchQuery& searchQuery) {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
if (search.empty()) {
|
||||
if (searchQuery.Empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto toLower = [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
};
|
||||
|
||||
std::string itemName = item->name;
|
||||
std::string searchText = search;
|
||||
std::transform(itemName.begin(), itemName.end(), itemName.begin(), toLower);
|
||||
std::transform(searchText.begin(), searchText.end(), searchText.begin(), toLower);
|
||||
return itemName.find(searchText) != std::string::npos;
|
||||
return searchQuery.Matches(item->name);
|
||||
}
|
||||
|
||||
bool ProjectPanel::IsCurrentTreeBranch(const std::string& currentFolderPath, const std::string& folderPath) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Panel.h"
|
||||
#include "Core/AssetItem.h"
|
||||
#include "UI/PopupState.h"
|
||||
#include "UI/SearchText.h"
|
||||
#include "UI/TreeView.h"
|
||||
|
||||
#include <functional>
|
||||
@@ -52,7 +53,7 @@ private:
|
||||
void RenderBrowserPane(IProjectManager& manager);
|
||||
void RenderBrowserHeader(IProjectManager& manager);
|
||||
AssetItemInteraction RenderAssetItem(const AssetItemPtr& item, bool isSelected);
|
||||
static bool MatchesSearch(const AssetItemPtr& item, const std::string& search);
|
||||
static bool MatchesSearch(const AssetItemPtr& item, const UI::SearchQuery& searchQuery);
|
||||
static bool IsCurrentTreeBranch(const std::string& currentFolderPath, const std::string& folderPath);
|
||||
|
||||
char m_searchBuffer[256] = "";
|
||||
|
||||
Reference in New Issue
Block a user