From a51e0f6f8866d623463ba08a09de21ab985c6c8a Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 27 Mar 2026 21:55:14 +0800 Subject: [PATCH] Unify editor divider and splitter chrome --- editor/src/UI/BaseTheme.h | 12 +- editor/src/UI/Core.h | 6 +- editor/src/UI/DividerChrome.h | 50 +++++ editor/src/UI/DockHostStyle.h | 9 +- editor/src/UI/DockTabBarChrome.h | 348 +++++++++++++++++++++++++++++ editor/src/UI/SplitterChrome.h | 69 ++++++ editor/src/UI/StyleTokens.h | 92 +++++++- editor/src/UI/UI.h | 3 + editor/src/UI/Widgets.h | 3 + editor/src/panels/ProjectPanel.cpp | 333 ++++++++++++++++++++++----- editor/src/panels/ProjectPanel.h | 17 +- 11 files changed, 866 insertions(+), 76 deletions(-) create mode 100644 editor/src/UI/DividerChrome.h create mode 100644 editor/src/UI/DockTabBarChrome.h create mode 100644 editor/src/UI/SplitterChrome.h diff --git a/editor/src/UI/BaseTheme.h b/editor/src/UI/BaseTheme.h index f3f67f73..4c33cd01 100644 --- a/editor/src/UI/BaseTheme.h +++ b/editor/src/UI/BaseTheme.h @@ -1,5 +1,6 @@ #pragma once +#include "SplitterChrome.h" #include "StyleTokens.h" #include @@ -9,8 +10,8 @@ namespace Editor { namespace UI { inline void ApplyBaseThemeColors(ImVec4* colors) { - colors[ImGuiCol_Text] = ImVec4(0.80f, 0.80f, 0.80f, 1.00f); - colors[ImGuiCol_TextDisabled] = ImVec4(0.53f, 0.53f, 0.53f, 1.00f); + colors[ImGuiCol_Text] = ImVec4(0.88f, 0.88f, 0.88f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.58f, 0.58f, 0.58f, 1.00f); colors[ImGuiCol_WindowBg] = ImVec4(0.22f, 0.22f, 0.22f, 1.00f); colors[ImGuiCol_ChildBg] = ImVec4(0.22f, 0.22f, 0.22f, 1.00f); colors[ImGuiCol_PopupBg] = ImVec4(0.17f, 0.17f, 0.17f, 0.98f); @@ -36,12 +37,7 @@ inline void ApplyBaseThemeColors(ImVec4* colors) { colors[ImGuiCol_Header] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); colors[ImGuiCol_HeaderHovered] = ImVec4(0.27f, 0.27f, 0.27f, 1.00f); colors[ImGuiCol_HeaderActive] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f); - colors[ImGuiCol_Separator] = ImVec4(0.13f, 0.13f, 0.13f, 1.00f); - colors[ImGuiCol_SeparatorHovered] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f); - colors[ImGuiCol_SeparatorActive] = ImVec4(0.34f, 0.34f, 0.34f, 1.00f); - colors[ImGuiCol_ResizeGrip] = ImVec4(0.24f, 0.24f, 0.24f, 0.00f); - colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.36f, 0.36f, 0.36f, 0.25f); - colors[ImGuiCol_ResizeGripActive] = ImVec4(0.52f, 0.52f, 0.52f, 0.25f); + ApplySplitterThemeColors(colors); colors[ImGuiCol_Tab] = DockTabColor(); colors[ImGuiCol_TabHovered] = DockTabHoveredColor(); colors[ImGuiCol_TabSelected] = DockTabSelectedColor(); diff --git a/editor/src/UI/Core.h b/editor/src/UI/Core.h index 8ebed7a3..51d6d4a2 100644 --- a/editor/src/UI/Core.h +++ b/editor/src/UI/Core.h @@ -1,5 +1,6 @@ #pragma once +#include "DividerChrome.h" #include "StyleTokens.h" #include @@ -141,10 +142,7 @@ inline void EndDisabled(bool disabled = true) { } inline void DrawCurrentWindowBottomBorder(ImU32 color = PanelDividerColor()) { - ImDrawList* drawList = ImGui::GetWindowDrawList(); - const ImVec2 min = ImGui::GetWindowPos(); - const ImVec2 max = ImVec2(min.x + ImGui::GetWindowSize().x, min.y + ImGui::GetWindowSize().y); - drawList->AddLine(ImVec2(min.x, max.y - 1.0f), ImVec2(max.x, max.y - 1.0f), color); + DrawCurrentWindowBottomDivider(color); } inline bool ToolbarButton(const char* label, bool active = false, ImVec2 size = ImVec2(0.0f, 0.0f)) { diff --git a/editor/src/UI/DividerChrome.h b/editor/src/UI/DividerChrome.h new file mode 100644 index 00000000..401ac348 --- /dev/null +++ b/editor/src/UI/DividerChrome.h @@ -0,0 +1,50 @@ +#pragma once + +#include "StyleTokens.h" + +#include + +namespace XCEngine { +namespace Editor { +namespace UI { + +inline void DrawHorizontalDivider( + ImDrawList* drawList, + float minX, + float maxX, + float y, + ImU32 color = PanelDividerColor(), + float thickness = PanelDividerThickness()) { + if (!drawList || maxX <= minX) { + return; + } + + drawList->AddLine(ImVec2(minX, y), ImVec2(maxX, y), color, thickness); +} + +inline void DrawVerticalDivider( + ImDrawList* drawList, + float x, + float minY, + float maxY, + ImU32 color = PanelDividerColor(), + float thickness = PanelDividerThickness()) { + if (!drawList || maxY <= minY) { + return; + } + + drawList->AddLine(ImVec2(x, minY), ImVec2(x, maxY), color, thickness); +} + +inline void DrawCurrentWindowBottomDivider( + ImU32 color = PanelDividerColor(), + float thickness = PanelDividerThickness()) { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const ImVec2 min = ImGui::GetWindowPos(); + const ImVec2 max = ImVec2(min.x + ImGui::GetWindowSize().x, min.y + ImGui::GetWindowSize().y); + DrawHorizontalDivider(drawList, min.x, max.x, max.y - 1.0f, color, thickness); +} + +} // namespace UI +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/UI/DockHostStyle.h b/editor/src/UI/DockHostStyle.h index 6a5ad043..1ffa775e 100644 --- a/editor/src/UI/DockHostStyle.h +++ b/editor/src/UI/DockHostStyle.h @@ -1,5 +1,6 @@ #pragma once +#include "SplitterChrome.h" #include "StyleTokens.h" #include @@ -24,10 +25,16 @@ public: ImGui::PushStyleColor(ImGuiCol_TabDimmed, DockTabDimmedColor()); ImGui::PushStyleColor(ImGuiCol_TabDimmedSelected, DockTabDimmedSelectedColor()); ImGui::PushStyleColor(ImGuiCol_TabDimmedSelectedOverline, DockTabDimmedSelectedOverlineColor()); + ImGui::PushStyleColor(ImGuiCol_Separator, PanelSplitterIdleColor()); + ImGui::PushStyleColor(ImGuiCol_SeparatorHovered, PanelSplitterHoveredColor()); + ImGui::PushStyleColor(ImGuiCol_SeparatorActive, PanelSplitterActiveColor()); + ImGui::PushStyleColor(ImGuiCol_ResizeGrip, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + ImGui::PushStyleColor(ImGuiCol_ResizeGripHovered, PanelSplitterHoveredColor()); + ImGui::PushStyleColor(ImGuiCol_ResizeGripActive, PanelSplitterActiveColor()); } ~DockHostStyleScope() { - ImGui::PopStyleColor(7); + ImGui::PopStyleColor(13); ImGui::PopStyleVar(5); } diff --git a/editor/src/UI/DockTabBarChrome.h b/editor/src/UI/DockTabBarChrome.h new file mode 100644 index 00000000..f8bcd653 --- /dev/null +++ b/editor/src/UI/DockTabBarChrome.h @@ -0,0 +1,348 @@ +#pragma once + +#include "DividerChrome.h" +#include "StyleTokens.h" + +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace UI { + +inline float DockedTabStripHeight() { + return 24.0f; +} + +inline float DockedTabHorizontalPadding() { + return 12.0f; +} + +inline float DockedTabTextOffsetY() { + return 1.0f; +} + +inline float DockedSelectedTabBottomOverlap() { + return 1.0f; +} + +inline std::unordered_map>& DockTabOrderCache() { + static std::unordered_map> cache; + return cache; +} + +inline ImGuiWindow* FindDockWindowByTabId(ImGuiDockNode& node, ImGuiID tabId) { + for (ImGuiWindow* window : node.Windows) { + if (window && window->TabId == tabId) { + return window; + } + } + return nullptr; +} + +inline const char* GetDockWindowLabelEnd(const ImGuiWindow& window) { + return ImGui::FindRenderedTextEnd(window.Name); +} + +inline int ResolveDockWindowDisplayOrder(const ImGuiWindow& window, int fallbackIndex) { + return window.DockOrder >= 0 ? window.DockOrder : 100000 + fallbackIndex; +} + +inline void SyncDockTabOrderCache(ImGuiDockNode& node) { + auto& cache = DockTabOrderCache(); + if (!node.IsLeafNode() || node.Windows.Size == 0) { + cache.erase(node.ID); + return; + } + + auto& order = cache[node.ID]; + auto containsWindow = [&node](ImGuiID tabId) { + return FindDockWindowByTabId(node, tabId) != nullptr; + }; + + order.erase( + std::remove_if( + order.begin(), + order.end(), + [&containsWindow](ImGuiID tabId) { + return !containsWindow(tabId); + }), + order.end()); + + if (order.empty()) { + std::vector> seededOrder; + seededOrder.reserve(node.Windows.Size); + for (int i = 0; i < node.Windows.Size; ++i) { + ImGuiWindow* window = node.Windows[i]; + if (!window) { + continue; + } + seededOrder.emplace_back(ResolveDockWindowDisplayOrder(*window, i), window->TabId); + } + std::stable_sort( + seededOrder.begin(), + seededOrder.end(), + [](const std::pair& lhs, const std::pair& rhs) { + return lhs.first < rhs.first; + }); + for (const auto& entry : seededOrder) { + order.push_back(entry.second); + } + return; + } + + std::vector> additions; + additions.reserve(node.Windows.Size); + for (int i = 0; i < node.Windows.Size; ++i) { + ImGuiWindow* window = node.Windows[i]; + if (!window) { + continue; + } + if (std::find(order.begin(), order.end(), window->TabId) == order.end()) { + additions.emplace_back(ResolveDockWindowDisplayOrder(*window, i), window->TabId); + } + } + + std::stable_sort( + additions.begin(), + additions.end(), + [](const std::pair& lhs, const std::pair& rhs) { + return lhs.first < rhs.first; + }); + for (const auto& entry : additions) { + order.push_back(entry.second); + } +} + +inline std::vector GetOrderedDockWindows(ImGuiDockNode& node) { + SyncDockTabOrderCache(node); + + std::vector orderedWindows; + orderedWindows.reserve(node.Windows.Size); + + auto& order = DockTabOrderCache()[node.ID]; + for (ImGuiID tabId : order) { + if (ImGuiWindow* window = FindDockWindowByTabId(node, tabId)) { + orderedWindows.push_back(window); + } + } + + for (ImGuiWindow* window : node.Windows) { + if (!window) { + continue; + } + if (std::find(orderedWindows.begin(), orderedWindows.end(), window) == orderedWindows.end()) { + orderedWindows.push_back(window); + } + } + + return orderedWindows; +} + +inline void ActivateDockWindow(ImGuiDockNode& node, ImGuiWindow& window) { + int currentIndex = -1; + for (int i = 0; i < node.Windows.Size; ++i) { + if (node.Windows[i] == &window) { + currentIndex = i; + break; + } + } + if (currentIndex <= 0) { + node.SelectedTabId = window.TabId; + node.VisibleWindow = &window; + return; + } + + ImGuiWindow* target = node.Windows[currentIndex]; + for (int i = currentIndex; i > 0; --i) { + node.Windows[i] = node.Windows[i - 1]; + } + node.Windows[0] = target; + node.SelectedTabId = window.TabId; + node.VisibleWindow = &window; +} + +inline bool IsDockWindowSelected(const ImGuiDockNode& node, const ImGuiWindow& window) { + return node.SelectedTabId ? node.SelectedTabId == window.TabId : node.VisibleWindow == &window; +} + +inline void ConfigureDockTabBarChrome(ImGuiDockNode* node) { + if (!node) { + return; + } + + if (node->IsLeafNode()) { + SyncDockTabOrderCache(*node); + + if (node->SelectedTabId == 0 && node->Windows.Size > 0 && node->Windows[0]) { + node->SelectedTabId = node->Windows[0]->TabId; + node->VisibleWindow = node->Windows[0]; + } + + if (ImGuiWindow* selectedWindow = FindDockWindowByTabId(*node, node->SelectedTabId)) { + ActivateDockWindow(*node, *selectedWindow); + } + + node->SetLocalFlags( + node->LocalFlags | + ImGuiDockNodeFlags_NoTabBar | + ImGuiDockNodeFlags_NoWindowMenuButton | + ImGuiDockNodeFlags_NoCloseButton); + } + + ConfigureDockTabBarChrome(node->ChildNodes[0]); + ConfigureDockTabBarChrome(node->ChildNodes[1]); +} + +inline void ConfigureDockTabBarChrome(ImGuiID dockspaceId) { + ConfigureDockTabBarChrome(ImGui::DockBuilderGetNode(dockspaceId)); +} + +inline ImU32 ResolveCustomDockTabColor(const ImGuiDockNode& node, const ImGuiWindow& window, bool hovered) { + if (IsDockWindowSelected(node, window)) { + return ImGui::GetColorU32(ImGuiCol_WindowBg); + } + + if (hovered) { + return window.DockStyle.Colors[ImGuiWindowDockStyleCol_TabHovered]; + } + + return window.DockStyle.Colors[ + node.IsFocused ? ImGuiWindowDockStyleCol_TabFocused : ImGuiWindowDockStyleCol_TabDimmed]; +} + +inline ImU32 ResolveCustomDockOverlineColor(const ImGuiDockNode& node, const ImGuiWindow& window) { + return window.DockStyle.Colors[ + node.IsFocused ? ImGuiWindowDockStyleCol_TabSelectedOverline : ImGuiWindowDockStyleCol_TabDimmedSelectedOverline]; +} + +inline void DrawCustomDockTab( + ImGuiDockNode& node, + ImGuiWindow& targetWindow, + const ImRect& tabRect, + const char* idSuffix) { + ImGui::SetCursorScreenPos(tabRect.Min); + ImGui::InvisibleButton(idSuffix, tabRect.GetSize()); + + const bool hovered = ImGui::IsItemHovered(); + const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImRect fillRect = tabRect; + if (IsDockWindowSelected(node, targetWindow)) { + fillRect.Max.y += DockedSelectedTabBottomOverlap(); + } + drawList->AddRectFilled(fillRect.Min, fillRect.Max, ResolveCustomDockTabColor(node, targetWindow, hovered)); + + if (IsDockWindowSelected(node, targetWindow)) { + const float y = tabRect.Min.y + 0.5f; + drawList->AddLine( + ImVec2(tabRect.Min.x, y), + ImVec2(tabRect.Max.x, y), + ResolveCustomDockOverlineColor(node, targetWindow), + 1.0f); + } + + const char* labelEnd = GetDockWindowLabelEnd(targetWindow); + const ImVec2 textSize = ImGui::CalcTextSize(targetWindow.Name, labelEnd, true); + ImRect textRect = tabRect; + textRect.Min.x += DockedTabHorizontalPadding(); + textRect.Max.x -= DockedTabHorizontalPadding(); + textRect.Min.y += DockedTabTextOffsetY(); + textRect.Max.y += DockedTabTextOffsetY(); + ImGui::RenderTextClipped( + textRect.Min, + textRect.Max, + targetWindow.Name, + labelEnd, + &textSize, + ImVec2(0.5f, 0.5f), + &tabRect); + + if (clicked) { + ActivateDockWindow(node, targetWindow); + ImGui::MarkIniSettingsDirty(); + } +} + +inline void DrawDockedWindowTabStrip() { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (!window || !window->DockNode || !window->DockNodeIsVisible) { + return; + } + + ImGuiDockNode& node = *window->DockNode; + if (!node.IsLeafNode() || node.Windows.Size == 0) { + return; + } + + const float stripHeight = DockedTabStripHeight(); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ToolbarBackgroundColor()); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + const bool open = ImGui::BeginChild( + "##DockTabStrip", + ImVec2(0.0f, stripHeight), + false, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(); + if (!open) { + ImGui::EndChild(); + return; + } + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const ImVec2 stripMin = ImGui::GetWindowPos(); + const ImVec2 stripSize = ImGui::GetWindowSize(); + const ImVec2 stripMax(stripMin.x + stripSize.x, stripMin.y + stripSize.y); + drawList->AddRectFilled(stripMin, stripMax, ImGui::GetColorU32(ToolbarBackgroundColor())); + + float cursorX = stripMin.x; + const std::vector orderedWindows = GetOrderedDockWindows(node); + float selectedTabMinX = stripMin.x; + float selectedTabMaxX = stripMin.x; + bool hasSelectedTab = false; + for (ImGuiWindow* dockedWindow : orderedWindows) { + if (!dockedWindow) { + continue; + } + + const char* labelEnd = GetDockWindowLabelEnd(*dockedWindow); + const ImVec2 textSize = ImGui::CalcTextSize(dockedWindow->Name, labelEnd, true); + const float tabWidth = textSize.x + DockedTabHorizontalPadding() * 2.0f; + const ImRect tabRect(cursorX, stripMin.y, cursorX + tabWidth, stripMin.y + stripHeight); + const ImGuiID pushId = dockedWindow->TabId ? dockedWindow->TabId : ImGui::GetID(dockedWindow->Name); + ImGui::PushID(pushId); + DrawCustomDockTab(node, *dockedWindow, tabRect, "##DockTab"); + ImGui::PopID(); + + if (IsDockWindowSelected(node, *dockedWindow)) { + selectedTabMinX = tabRect.Min.x; + selectedTabMaxX = tabRect.Max.x; + hasSelectedTab = true; + } + + cursorX += tabWidth; + } + + const float dividerY = stripMax.y - 0.5f; + if (hasSelectedTab) { + if (selectedTabMinX > stripMin.x) { + DrawHorizontalDivider(drawList, stripMin.x, selectedTabMinX, dividerY); + } + if (selectedTabMaxX < stripMax.x) { + DrawHorizontalDivider(drawList, selectedTabMaxX, stripMax.x, dividerY); + } + } else { + DrawHorizontalDivider(drawList, stripMin.x, stripMax.x, dividerY); + } + + ImGui::EndChild(); +} + +} // namespace UI +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/UI/SplitterChrome.h b/editor/src/UI/SplitterChrome.h new file mode 100644 index 00000000..a99e74a5 --- /dev/null +++ b/editor/src/UI/SplitterChrome.h @@ -0,0 +1,69 @@ +#pragma once + +#include "StyleTokens.h" + +#include + +namespace XCEngine { +namespace Editor { +namespace UI { + +enum class SplitterAxis { + Vertical, + Horizontal +}; + +struct SplitterResult { + bool hovered = false; + bool active = false; + float delta = 0.0f; +}; + +inline void ApplySplitterThemeColors(ImVec4* colors) { + colors[ImGuiCol_Separator] = PanelSplitterIdleColor(); + colors[ImGuiCol_SeparatorHovered] = PanelSplitterHoveredColor(); + colors[ImGuiCol_SeparatorActive] = PanelSplitterActiveColor(); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + colors[ImGuiCol_ResizeGripHovered] = PanelSplitterHoveredColor(); + colors[ImGuiCol_ResizeGripActive] = PanelSplitterActiveColor(); +} + +inline SplitterResult DrawSplitter( + const char* id, + SplitterAxis axis, + float length, + float hitThickness = PanelSplitterHitThickness()) { + const bool vertical = axis == SplitterAxis::Vertical; + const ImVec2 min = ImGui::GetCursorScreenPos(); + const ImVec2 size = vertical ? ImVec2(hitThickness, length) : ImVec2(length, hitThickness); + ImGui::InvisibleButton(id, size); + + const bool hovered = ImGui::IsItemHovered(); + const bool active = ImGui::IsItemActive(); + const float delta = active + ? (vertical ? ImGui::GetIO().MouseDelta.x : ImGui::GetIO().MouseDelta.y) + : 0.0f; + + if (hovered || active) { + ImGui::SetMouseCursor(vertical ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS); + } + + const ImVec4 color = active + ? PanelSplitterActiveColor() + : (hovered ? PanelSplitterHoveredColor() : PanelSplitterIdleColor()); + const float thickness = PanelSplitterVisibleThickness(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + if (vertical) { + const float centerX = min.x + (size.x * 0.5f); + drawList->AddLine(ImVec2(centerX, min.y), ImVec2(centerX, min.y + size.y), ImGui::GetColorU32(color), thickness); + } else { + const float centerY = min.y + (size.y * 0.5f); + drawList->AddLine(ImVec2(min.x, centerY), ImVec2(min.x + size.x, centerY), ImGui::GetColorU32(color), thickness); + } + + return SplitterResult{ hovered, active, delta }; +} + +} // namespace UI +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/UI/StyleTokens.h b/editor/src/UI/StyleTokens.h index 23b7848a..859c0219 100644 --- a/editor/src/UI/StyleTokens.h +++ b/editor/src/UI/StyleTokens.h @@ -58,6 +58,10 @@ inline ImVec2 PanelWindowPadding() { return ImVec2(0.0f, 0.0f); } +inline ImVec4 HierarchyInspectorPanelBackgroundColor() { + return ImVec4(0.235f, 0.235f, 0.235f, 1.0f); +} + inline ImVec2 ToolbarPadding() { return ImVec2(8.0f, 6.0f); } @@ -71,7 +75,7 @@ inline float StandardPanelToolbarHeight() { } inline float ProjectPanelToolbarHeight() { - return 60.0f; + return 34.0f; } inline float ToolbarSearchTrailingSpacing() { @@ -114,6 +118,66 @@ inline ImVec2 AssetPanelContentPadding() { return ImVec2(10.0f, 10.0f); } +inline float ProjectBrowserHeaderHeight() { + return 28.0f; +} + +inline float ProjectNavigationDefaultWidth() { + return 248.0f; +} + +inline float ProjectNavigationMinWidth() { + return 180.0f; +} + +inline float ProjectBrowserMinWidth() { + return 260.0f; +} + +inline float PanelSplitterHitThickness() { + return 4.0f; +} + +inline float PanelSplitterVisibleThickness() { + return 1.0f; +} + +inline ImVec4 PanelSplitterIdleColor() { + return ImVec4(0.14f, 0.14f, 0.14f, 1.0f); +} + +inline ImVec4 PanelSplitterHoveredColor() { + return ImVec4(0.30f, 0.30f, 0.30f, 1.0f); +} + +inline ImVec4 PanelSplitterActiveColor() { + return ImVec4(0.34f, 0.34f, 0.34f, 1.0f); +} + +inline ImVec2 ProjectNavigationPanePadding() { + return ImVec2(8.0f, 6.0f); +} + +inline ImVec2 ProjectBrowserPanePadding() { + return ImVec2(10.0f, 8.0f); +} + +inline ImVec2 ProjectTreeNodeFramePadding() { + return ImVec2(4.0f, 3.0f); +} + +inline ImVec4 ProjectNavigationPaneBackgroundColor() { + return ImVec4(0.20f, 0.20f, 0.20f, 1.0f); +} + +inline ImVec4 ProjectBrowserHeaderBackgroundColor() { + return ImVec4(0.18f, 0.18f, 0.18f, 1.0f); +} + +inline ImVec4 ProjectBrowserPaneBackgroundColor() { + return ImVec4(0.22f, 0.22f, 0.22f, 1.0f); +} + inline ImVec4 ToolbarBackgroundColor() { return ImVec4(0.19f, 0.19f, 0.19f, 1.0f); } @@ -127,7 +191,11 @@ inline float SearchFieldFrameRounding() { } inline ImU32 PanelDividerColor() { - return IM_COL32(36, 36, 36, 255); + return ImGui::GetColorU32(PanelSplitterIdleColor()); +} + +inline float PanelDividerThickness() { + return 1.0f; } inline ImVec4 EmptyStateSubtitleColor() { @@ -186,6 +254,14 @@ inline float AssetTileRounding() { return 2.0f; } +inline ImVec4 AssetTileIdleFillColor() { + return ImVec4(1.0f, 1.0f, 1.0f, 0.02f); +} + +inline ImVec4 AssetTileIdleBorderColor() { + return ImVec4(1.0f, 1.0f, 1.0f, 0.05f); +} + inline ImVec4 AssetTileHoverFillColor() { return ImVec4(1.0f, 1.0f, 1.0f, 0.04f); } @@ -203,11 +279,11 @@ inline ImVec4 AssetTileDraggedOverlayColor() { } inline ImVec2 AssetTileIconOffset() { - return ImVec2(14.0f, 12.0f); + return ImVec2(12.0f, 10.0f); } inline ImVec2 AssetTileIconSize() { - return ImVec2(28.0f, 22.0f); + return ImVec2(32.0f, 24.0f); } inline ImVec2 AssetTileTextPadding() { @@ -219,19 +295,19 @@ inline ImVec4 AssetTileTextColor(bool selected = false) { } inline ImVec4 AssetFolderIconFillColor() { - return ImVec4(0.46f, 0.46f, 0.46f, 1.0f); + return ImVec4(0.50f, 0.50f, 0.50f, 1.0f); } inline ImVec4 AssetFolderIconLineColor() { - return ImVec4(0.72f, 0.72f, 0.72f, 0.86f); + return ImVec4(0.80f, 0.80f, 0.80f, 0.90f); } inline ImVec4 AssetFileIconFillColor() { - return ImVec4(0.41f, 0.41f, 0.41f, 1.0f); + return ImVec4(0.46f, 0.46f, 0.46f, 1.0f); } inline ImVec4 AssetFileIconLineColor() { - return ImVec4(0.65f, 0.65f, 0.65f, 0.86f); + return ImVec4(0.74f, 0.74f, 0.74f, 0.90f); } inline ImVec4 AssetFileFoldColor() { diff --git a/editor/src/UI/UI.h b/editor/src/UI/UI.h index 1b84c73e..e4bb563d 100644 --- a/editor/src/UI/UI.h +++ b/editor/src/UI/UI.h @@ -6,9 +6,12 @@ #include "ConsoleLogFormatter.h" #include "Core.h" #include "DockHostStyle.h" +#include "DockTabBarChrome.h" +#include "DividerChrome.h" #include "PanelChrome.h" #include "PopupState.h" #include "PropertyGrid.h" +#include "SplitterChrome.h" #include "ScalarControls.h" #include "SceneStatusWidget.h" #include "StyleTokens.h" diff --git a/editor/src/UI/Widgets.h b/editor/src/UI/Widgets.h index d6258037..8b409fd4 100644 --- a/editor/src/UI/Widgets.h +++ b/editor/src/UI/Widgets.h @@ -232,6 +232,9 @@ inline AssetTileResult DrawAssetTile( const ImVec2 max = ImVec2(min.x + tileSize.x, min.y + tileSize.y); ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(min, max, ImGui::GetColorU32(AssetTileIdleFillColor()), AssetTileRounding()); + drawList->AddRect(min, max, ImGui::GetColorU32(AssetTileIdleBorderColor()), AssetTileRounding()); + if (hovered || selected) { drawList->AddRectFilled(min, max, ImGui::GetColorU32(selected ? AssetTileSelectedFillColor() : AssetTileHoverFillColor()), AssetTileRounding()); } diff --git a/editor/src/panels/ProjectPanel.cpp b/editor/src/panels/ProjectPanel.cpp index 36833615..c21882b5 100644 --- a/editor/src/panels/ProjectPanel.cpp +++ b/editor/src/panels/ProjectPanel.cpp @@ -6,6 +6,9 @@ #include "Core/IProjectManager.h" #include "Core/AssetItem.h" #include "UI/UI.h" + +#include +#include #include 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 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((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(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(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; } } diff --git a/editor/src/panels/ProjectPanel.h b/editor/src/panels/ProjectPanel.h index cf309dd8..f4ada144 100644 --- a/editor/src/panels/ProjectPanel.h +++ b/editor/src/panels/ProjectPanel.h @@ -14,9 +14,24 @@ public: void Initialize(const std::string& projectPath); private: - void RenderAssetItem(const AssetItemPtr& item, int index); + struct AssetItemInteraction { + bool clicked = false; + bool contextRequested = false; + bool openRequested = false; + std::string droppedSourcePath; + }; + + void RenderToolbar(); + void RenderFolderTreePane(IProjectManager& manager); + void RenderFolderTreeNode(IProjectManager& manager, const AssetItemPtr& folder, const std::string& currentFolderPath); + 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 IsCurrentTreeBranch(const std::string& currentFolderPath, const std::string& folderPath); char m_searchBuffer[256] = ""; + float m_navigationWidth = UI::ProjectNavigationDefaultWidth(); UI::TextInputPopupState<256> m_createFolderDialog; UI::DeferredPopupState m_emptyContextMenu; UI::TargetedPopupState m_itemContextMenu;