Unify editor divider and splitter chrome
This commit is contained in:
@@ -6,6 +6,9 @@
|
||||
#include "Core/IProjectManager.h"
|
||||
#include "Core/AssetItem.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <imgui.h>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -25,32 +28,160 @@ void ProjectPanel::Render() {
|
||||
}
|
||||
|
||||
Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Project);
|
||||
|
||||
|
||||
auto& manager = m_context->GetProjectManager();
|
||||
RenderToolbar();
|
||||
|
||||
UI::PanelToolbarScope toolbar("ProjectToolbar", UI::ProjectPanelToolbarHeight());
|
||||
if (toolbar.IsOpen()) {
|
||||
Actions::DrawProjectNavigateBackAction(manager);
|
||||
ImGui::SameLine();
|
||||
|
||||
size_t pathDepth = manager.GetPathDepth();
|
||||
UI::DrawToolbarBreadcrumbs(
|
||||
"Assets",
|
||||
pathDepth,
|
||||
[&](size_t index) { return manager.GetPathName(index); },
|
||||
[&](size_t index) { manager.NavigateToIndex(index); });
|
||||
|
||||
UI::DrawToolbarRowGap();
|
||||
UI::ToolbarSearchField("##Search", "Search assets", m_searchBuffer, sizeof(m_searchBuffer));
|
||||
UI::PanelContentScope content("ProjectContent", ImVec2(0.0f, 0.0f));
|
||||
if (!content.IsOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UI::PanelContentScope content(
|
||||
"ProjectContent",
|
||||
UI::AssetPanelContentPadding(),
|
||||
ImGuiWindowFlags_None,
|
||||
true,
|
||||
UI::AssetGridSpacing());
|
||||
if (!content.IsOpen()) {
|
||||
const float totalHeight = ImGui::GetContentRegionAvail().y;
|
||||
const float splitterWidth = UI::PanelSplitterHitThickness();
|
||||
const float availableWidth = ImGui::GetContentRegionAvail().x;
|
||||
const float clampedNavigationWidth = std::clamp(
|
||||
m_navigationWidth,
|
||||
UI::ProjectNavigationMinWidth(),
|
||||
std::max(UI::ProjectNavigationMinWidth(), availableWidth - UI::ProjectBrowserMinWidth() - splitterWidth));
|
||||
m_navigationWidth = clampedNavigationWidth;
|
||||
|
||||
RenderFolderTreePane(manager);
|
||||
ImGui::SameLine(0.0f, 0.0f);
|
||||
const UI::SplitterResult splitter = UI::DrawSplitter("##ProjectPaneSplitter", UI::SplitterAxis::Vertical, totalHeight);
|
||||
if (splitter.active) {
|
||||
m_navigationWidth += splitter.delta;
|
||||
}
|
||||
ImGui::SameLine(0.0f, 0.0f);
|
||||
RenderBrowserPane(manager);
|
||||
|
||||
Actions::DrawProjectCreateFolderDialog(*m_context, m_createFolderDialog);
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderToolbar() {
|
||||
UI::PanelToolbarScope toolbar("ProjectToolbar", UI::ProjectPanelToolbarHeight());
|
||||
if (!toolbar.IsOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::BeginTable("##ProjectToolbarLayout", 2, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) {
|
||||
ImGui::TableSetupColumn("##Spacer", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 220.0f);
|
||||
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
UI::ToolbarSearchField("##Search", "Search assets", m_searchBuffer, sizeof(m_searchBuffer));
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding());
|
||||
const bool open = ImGui::BeginChild("ProjectFolderTree", ImVec2(m_navigationWidth, 0.0f), false);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
if (!open) {
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
const AssetItemPtr rootFolder = manager.GetRootFolder();
|
||||
const AssetItemPtr currentFolder = manager.GetCurrentFolder();
|
||||
const std::string currentFolderPath = currentFolder ? currentFolder->fullPath : std::string();
|
||||
if (rootFolder) {
|
||||
RenderFolderTreeNode(manager, rootFolder, currentFolderPath);
|
||||
} else {
|
||||
UI::DrawEmptyState("No Assets Folder");
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderFolderTreeNode(
|
||||
IProjectManager& manager,
|
||||
const AssetItemPtr& folder,
|
||||
const std::string& currentFolderPath) {
|
||||
if (!folder || !folder->isFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasChildFolders = false;
|
||||
for (const auto& child : folder->children) {
|
||||
if (child && child->isFolder) {
|
||||
hasChildFolders = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiTreeNodeFlags flags =
|
||||
ImGuiTreeNodeFlags_OpenOnArrow |
|
||||
ImGuiTreeNodeFlags_OpenOnDoubleClick |
|
||||
ImGuiTreeNodeFlags_SpanAvailWidth |
|
||||
ImGuiTreeNodeFlags_FramePadding;
|
||||
if (!hasChildFolders) {
|
||||
flags |= ImGuiTreeNodeFlags_Leaf;
|
||||
}
|
||||
if (folder->fullPath == currentFolderPath) {
|
||||
flags |= ImGuiTreeNodeFlags_Selected;
|
||||
}
|
||||
if (IsCurrentTreeBranch(currentFolderPath, folder->fullPath)) {
|
||||
flags |= ImGuiTreeNodeFlags_DefaultOpen;
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, UI::ProjectTreeNodeFramePadding());
|
||||
const bool open = ImGui::TreeNodeEx((void*)folder.get(), flags, "%s", folder->name.c_str());
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
if (ImGui::IsItemClicked()) {
|
||||
manager.NavigateToFolder(folder);
|
||||
}
|
||||
|
||||
if (open) {
|
||||
for (const auto& child : folder->children) {
|
||||
if (!child || !child->isFolder) {
|
||||
continue;
|
||||
}
|
||||
RenderFolderTreeNode(manager, child, currentFolderPath);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
const bool open = ImGui::BeginChild("ProjectBrowser", ImVec2(0.0f, 0.0f), false);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
if (!open) {
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<AssetItemPtr> visibleItems;
|
||||
const auto& items = manager.GetCurrentItems();
|
||||
const std::string search = m_searchBuffer;
|
||||
visibleItems.reserve(items.size());
|
||||
for (const auto& item : items) {
|
||||
if (MatchesSearch(item, search)) {
|
||||
visibleItems.push_back(item);
|
||||
}
|
||||
}
|
||||
|
||||
RenderBrowserHeader(manager);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectBrowserPanePadding());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::AssetGridSpacing());
|
||||
const bool bodyOpen = ImGui::BeginChild("ProjectBrowserBody", ImVec2(0.0f, 0.0f), false);
|
||||
ImGui::PopStyleVar(2);
|
||||
if (!bodyOpen) {
|
||||
ImGui::EndChild();
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,45 +189,106 @@ void ProjectPanel::Render() {
|
||||
const float spacing = UI::AssetGridSpacing().x;
|
||||
const float panelWidth = ImGui::GetContentRegionAvail().x;
|
||||
int columns = static_cast<int>((panelWidth + spacing) / (tileWidth + spacing));
|
||||
if (columns < 1) columns = 1;
|
||||
|
||||
auto& items = manager.GetCurrentItems();
|
||||
std::string searchStr = m_searchBuffer;
|
||||
int displayedCount = 0;
|
||||
|
||||
for (int i = 0; i < (int)items.size(); i++) {
|
||||
if (!searchStr.empty()) {
|
||||
if (items[i]->name.find(searchStr) == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (displayedCount > 0 && displayedCount % columns != 0) {
|
||||
ImGui::SameLine();
|
||||
}
|
||||
RenderAssetItem(items[i], i);
|
||||
displayedCount++;
|
||||
if (columns < 1) {
|
||||
columns = 1;
|
||||
}
|
||||
|
||||
if (displayedCount == 0) {
|
||||
AssetItemPtr pendingSelection;
|
||||
AssetItemPtr pendingContextTarget;
|
||||
AssetItemPtr pendingOpenTarget;
|
||||
AssetItemPtr pendingMoveTarget;
|
||||
std::string pendingMoveSourcePath;
|
||||
|
||||
const std::string selectedItemPath = manager.GetSelectedItemPath();
|
||||
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
|
||||
if (visibleIndex > 0 && visibleIndex % columns != 0) {
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
const AssetItemPtr& item = visibleItems[visibleIndex];
|
||||
const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath);
|
||||
if (interaction.clicked) {
|
||||
pendingSelection = item;
|
||||
}
|
||||
if (interaction.contextRequested) {
|
||||
pendingContextTarget = item;
|
||||
}
|
||||
if (!interaction.droppedSourcePath.empty()) {
|
||||
pendingMoveSourcePath = interaction.droppedSourcePath;
|
||||
pendingMoveTarget = item;
|
||||
break;
|
||||
}
|
||||
if (interaction.openRequested) {
|
||||
pendingOpenTarget = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (visibleItems.empty()) {
|
||||
UI::DrawEmptyState(
|
||||
searchStr.empty() ? "No Assets" : "No Search Results",
|
||||
searchStr.empty() ? "Current folder is empty" : "No assets match the current search");
|
||||
search.empty() ? "No Assets" : "No Search Results",
|
||||
search.empty() ? "Current folder is empty" : "No assets match the current search");
|
||||
}
|
||||
|
||||
Actions::HandleProjectBackgroundPrimaryClick(manager);
|
||||
if (pendingSelection) {
|
||||
manager.SetSelectedItem(pendingSelection);
|
||||
}
|
||||
if (pendingContextTarget) {
|
||||
Actions::HandleProjectItemContextRequest(manager, pendingContextTarget, m_itemContextMenu);
|
||||
}
|
||||
if (!pendingMoveSourcePath.empty() && pendingMoveTarget) {
|
||||
Commands::MoveAssetToFolder(manager, pendingMoveSourcePath, pendingMoveTarget);
|
||||
}
|
||||
if (pendingOpenTarget) {
|
||||
Actions::OpenProjectAsset(*m_context, pendingOpenTarget);
|
||||
}
|
||||
Actions::DrawProjectItemContextPopup(*m_context, m_itemContextMenu);
|
||||
Actions::RequestProjectEmptyContextPopup(m_emptyContextMenu);
|
||||
Actions::DrawProjectEmptyContextPopup(m_emptyContextMenu, m_createFolderDialog);
|
||||
|
||||
Actions::DrawProjectCreateFolderDialog(*m_context, m_createFolderDialog);
|
||||
ImGui::EndChild();
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) {
|
||||
auto& manager = m_context->GetProjectManager();
|
||||
bool isSelected = (manager.GetSelectedIndex() == index);
|
||||
|
||||
ImGui::PushID(index);
|
||||
void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) {
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserHeaderBackgroundColor());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 5.0f));
|
||||
const bool open = ImGui::BeginChild(
|
||||
"ProjectBrowserHeader",
|
||||
ImVec2(0.0f, UI::ProjectBrowserHeaderHeight()),
|
||||
false,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
if (!open) {
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::BeginTable("##ProjectBrowserHeaderLayout", 1, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX)) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
UI::DrawToolbarBreadcrumbs(
|
||||
"Assets",
|
||||
manager.GetPathDepth(),
|
||||
[&](size_t index) { return manager.GetPathName(index); },
|
||||
[&](size_t index) { manager.NavigateToIndex(index); });
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
const ImVec2 min = ImGui::GetWindowPos();
|
||||
const ImVec2 max(min.x + ImGui::GetWindowSize().x, min.y + ImGui::GetWindowSize().y);
|
||||
UI::DrawHorizontalDivider(drawList, min.x, max.x, max.y - 0.5f);
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItemPtr& item, bool isSelected) {
|
||||
AssetItemInteraction interaction;
|
||||
|
||||
ImGui::PushID(item ? item->fullPath.c_str() : "ProjectItem");
|
||||
const bool isDraggingThisItem = Actions::IsProjectAssetBeingDragged(item);
|
||||
const UI::AssetIconKind iconKind = item->isFolder ? UI::AssetIconKind::Folder : UI::AssetIconKind::File;
|
||||
|
||||
@@ -106,24 +298,57 @@ void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) {
|
||||
isDraggingThisItem,
|
||||
[&](ImDrawList* drawList, const ImVec2& iconMin, const ImVec2& iconMax) {
|
||||
UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind);
|
||||
});
|
||||
});
|
||||
|
||||
if (tile.clicked) {
|
||||
Actions::HandleProjectItemSelection(manager, index);
|
||||
interaction.clicked = true;
|
||||
}
|
||||
|
||||
if (tile.contextRequested) {
|
||||
Actions::HandleProjectItemContextRequest(manager, index, item, m_itemContextMenu);
|
||||
interaction.contextRequested = true;
|
||||
}
|
||||
|
||||
Actions::AcceptProjectAssetDrop(manager, item);
|
||||
interaction.droppedSourcePath = Actions::AcceptProjectAssetDropPayload(item);
|
||||
Actions::BeginProjectAssetDrag(item, iconKind);
|
||||
|
||||
if (tile.openRequested) {
|
||||
Actions::OpenProjectAsset(*m_context, item);
|
||||
interaction.openRequested = true;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
return interaction;
|
||||
}
|
||||
|
||||
bool ProjectPanel::MatchesSearch(const AssetItemPtr& item, const std::string& search) {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
if (search.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;
|
||||
}
|
||||
|
||||
bool ProjectPanel::IsCurrentTreeBranch(const std::string& currentFolderPath, const std::string& folderPath) {
|
||||
if (currentFolderPath.empty() || folderPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
if (currentFolderPath == folderPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string withForwardSlash = folderPath + "/";
|
||||
const std::string withBackwardSlash = folderPath + "\\";
|
||||
return currentFolderPath.rfind(withForwardSlash, 0) == 0 || currentFolderPath.rfind(withBackwardSlash, 0) == 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user