diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 6577e877..d5028f84 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -29,6 +29,7 @@ set(IMGUI_SOURCES ) add_executable(${PROJECT_NAME} WIN32 + src/EditorApp.rc src/main.cpp src/Application.cpp src/Theme.cpp @@ -45,6 +46,7 @@ add_executable(${PROJECT_NAME} WIN32 src/panels/InspectorPanel.cpp src/panels/ConsolePanel.cpp src/panels/ProjectPanel.cpp + src/UI/BuiltInIcons.cpp src/Layers/EditorLayer.cpp ${IMGUI_SOURCES} ) @@ -54,6 +56,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE ${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends ${CMAKE_CURRENT_SOURCE_DIR}/../engine/include + ${CMAKE_CURRENT_SOURCE_DIR}/../engine/third_party/stb ${CMAKE_CURRENT_SOURCE_DIR}/../tests/OpenGL/package/glm ) diff --git a/editor/project.png b/editor/project.png new file mode 100644 index 00000000..77a0c04f Binary files /dev/null and b/editor/project.png differ diff --git a/editor/rename_color.py b/editor/rename_color.py new file mode 100644 index 00000000..9999e203 --- /dev/null +++ b/editor/rename_color.py @@ -0,0 +1,28 @@ +import os +from PIL import Image + + +def get_dominant_color(image_path): + img = Image.open(image_path).convert("RGB") + img = img.resize((1, 1), Image.Resampling.LANCZOS) + r, g, b = img.getpixel((0, 0)) + return r, g, b + + +def rename_with_color(base_path): + files = ["color.png", "color2.png"] + for f in files: + old_path = os.path.join(base_path, f) + if os.path.exists(old_path): + r, g, b = get_dominant_color(old_path) + new_name = f"color-({r},{g},{b}).png" + new_path = os.path.join(base_path, new_name) + os.rename(old_path, new_path) + print(f"Renamed: {f} -> {new_name}") + else: + print(f"File not found: {old_path}") + + +if __name__ == "__main__": + base = r"D:\Xuanchi\Main\XCEngine\editor" + rename_with_color(base) diff --git a/editor/resources/Icons/app.ico b/editor/resources/Icons/app.ico new file mode 100644 index 00000000..ecec9510 Binary files /dev/null and b/editor/resources/Icons/app.ico differ diff --git a/editor/resources/Icons/folder_empty_icon.png b/editor/resources/Icons/folder_empty_icon.png new file mode 100644 index 00000000..247b25a4 Binary files /dev/null and b/editor/resources/Icons/folder_empty_icon.png differ diff --git a/editor/resources/Icons/folder_icon.png b/editor/resources/Icons/folder_icon.png new file mode 100644 index 00000000..55d061ce Binary files /dev/null and b/editor/resources/Icons/folder_icon.png differ diff --git a/editor/resources/Icons/gameobject_icon.png b/editor/resources/Icons/gameobject_icon.png new file mode 100644 index 00000000..39f08e89 Binary files /dev/null and b/editor/resources/Icons/gameobject_icon.png differ diff --git a/editor/resources/Icons/logo_icon.png b/editor/resources/Icons/logo_icon.png new file mode 100644 index 00000000..b9760f6d Binary files /dev/null and b/editor/resources/Icons/logo_icon.png differ diff --git a/editor/resources/Icons/resize_png.py b/editor/resources/Icons/resize_png.py new file mode 100644 index 00000000..61c45088 --- /dev/null +++ b/editor/resources/Icons/resize_png.py @@ -0,0 +1,25 @@ +from PIL import Image +import os +import sys + + +def resize_png(input_path, output_path, size=(30, 30)): + img = Image.open(input_path) + img = img.resize(size, Image.Resampling.LANCZOS) + img.save(output_path, "PNG") + print(f"Resized {input_path} to {output_path}") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python resize_png.py [output.png]") + sys.exit(1) + + input_file = sys.argv[1] + output_file = ( + sys.argv[2] + if len(sys.argv) > 2 + else f"{os.path.splitext(input_file)[0]}_30x30.png" + ) + + resize_png(input_file, output_file) diff --git a/editor/resources/Icons/简单模型.png b/editor/resources/Icons/简单模型.png deleted file mode 100644 index 65120ae0..00000000 Binary files a/editor/resources/Icons/简单模型.png and /dev/null differ diff --git a/editor/src/Actions/EditActionRouter.h b/editor/src/Actions/EditActionRouter.h index 12e1908d..656009c4 100644 --- a/editor/src/Actions/EditActionRouter.h +++ b/editor/src/Actions/EditActionRouter.h @@ -77,12 +77,11 @@ inline bool ExecuteOpenSelection(IEditorContext& context, const EditActionTarget inline bool ExecuteDeleteSelection(IEditorContext& context, const EditActionTarget& target) { if (target.route == EditorActionRoute::Project) { auto& projectManager = context.GetProjectManager(); - if (!target.selectedAssetItem || projectManager.GetSelectedIndex() < 0) { + if (!target.selectedAssetItem) { return false; } - Commands::DeleteAsset(projectManager, projectManager.GetSelectedIndex()); - return true; + return Commands::DeleteAsset(projectManager, target.selectedAssetItem); } if (target.route != EditorActionRoute::Hierarchy || !target.selectedGameObject) { diff --git a/editor/src/Actions/EditorActions.h b/editor/src/Actions/EditorActions.h index 1f753168..de23957e 100644 --- a/editor/src/Actions/EditorActions.h +++ b/editor/src/Actions/EditorActions.h @@ -186,14 +186,7 @@ inline ::XCEngine::Components::GameObject* GetSelectedGameObject(IEditorContext& } inline AssetItemPtr GetSelectedAssetItem(IEditorContext& context) { - auto& projectManager = context.GetProjectManager(); - const int selectedIndex = projectManager.GetSelectedIndex(); - auto& items = projectManager.GetCurrentItems(); - if (selectedIndex < 0 || selectedIndex >= static_cast(items.size())) { - return nullptr; - } - - return items[selectedIndex]; + return context.GetProjectManager().GetSelectedItem(); } } // namespace Actions diff --git a/editor/src/Actions/MainMenuActionRouter.h b/editor/src/Actions/MainMenuActionRouter.h index 9c9f9383..218b9a11 100644 --- a/editor/src/Actions/MainMenuActionRouter.h +++ b/editor/src/Actions/MainMenuActionRouter.h @@ -97,7 +97,6 @@ inline void DrawMainMenuBar(IEditorContext& context, UI::DeferredPopupState& abo UI::DrawMenuScope("Help", [&]() { DrawHelpMenuActions(aboutPopup); }); - UI::DrawSceneStatusWidget(context); ImGui::EndMainMenuBar(); } diff --git a/editor/src/Actions/ProjectActionRouter.h b/editor/src/Actions/ProjectActionRouter.h index 984e9722..5a3a0461 100644 --- a/editor/src/Actions/ProjectActionRouter.h +++ b/editor/src/Actions/ProjectActionRouter.h @@ -4,6 +4,7 @@ #include "Commands/ProjectCommands.h" #include "Core/IEditorContext.h" #include "Core/IProjectManager.h" +#include "UI/BuiltInIcons.h" #include "UI/PopupState.h" namespace XCEngine { @@ -16,25 +17,6 @@ inline constexpr const char* ProjectAssetPayloadType() { inline void DrawProjectAssetContextActions(IEditorContext& context, const AssetItemPtr& item); -inline int FindProjectItemIndex(IProjectManager& projectManager, const AssetItemPtr& item) { - if (!item) { - return -1; - } - - const auto& items = projectManager.GetCurrentItems(); - for (size_t i = 0; i < items.size(); ++i) { - if (items[i] == item) { - return static_cast(i); - } - - if (items[i] && items[i]->fullPath == item->fullPath) { - return static_cast(i); - } - } - - return -1; -} - inline const char* GetDraggedProjectAssetPath() { const ImGuiPayload* payload = ImGui::GetDragDropPayload(); if (!payload || !payload->IsDataType(ProjectAssetPayloadType())) { @@ -49,18 +31,20 @@ inline bool IsProjectAssetBeingDragged(const AssetItemPtr& item) { return item != nullptr && draggedPath != nullptr && item->fullPath == draggedPath; } -inline bool AcceptProjectAssetDrop(IProjectManager& projectManager, const AssetItemPtr& targetFolder) { +inline std::string AcceptProjectAssetDropPayload(const AssetItemPtr& targetFolder) { if (!targetFolder || !targetFolder->isFolder || !ImGui::BeginDragDropTarget()) { - return false; + return {}; } - bool accepted = false; + std::string draggedPath; if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ProjectAssetPayloadType())) { - const char* draggedPath = static_cast(payload->Data); - accepted = Commands::MoveAssetToFolder(projectManager, draggedPath, targetFolder); + const char* payloadPath = static_cast(payload->Data); + if (payloadPath) { + draggedPath = payloadPath; + } } ImGui::EndDragDropTarget(); - return accepted; + return draggedPath; } inline bool BeginProjectAssetDrag(const AssetItemPtr& item, UI::AssetIconKind iconKind) { @@ -100,7 +84,7 @@ inline bool DrawProjectNavigateBackAction(IProjectManager& projectManager) { inline void HandleProjectBackgroundPrimaryClick(IProjectManager& projectManager) { if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered()) { - projectManager.SetSelectedIndex(-1); + projectManager.ClearSelection(); } } @@ -110,16 +94,15 @@ inline void RequestProjectEmptyContextPopup(UI::DeferredPopupState& emptyContext } } -inline void HandleProjectItemSelection(IProjectManager& projectManager, int index) { - projectManager.SetSelectedIndex(index); +inline void HandleProjectItemSelection(IProjectManager& projectManager, const AssetItemPtr& item) { + projectManager.SetSelectedItem(item); } inline void HandleProjectItemContextRequest( IProjectManager& projectManager, - int index, const AssetItemPtr& item, UI::TargetedPopupState& itemContextMenu) { - projectManager.SetSelectedIndex(index); + projectManager.SetSelectedItem(item); itemContextMenu.RequestOpen(item); } @@ -139,14 +122,14 @@ inline void DrawProjectItemContextPopup(IEditorContext& context, UI::TargetedPop inline void DrawProjectAssetContextActions(IEditorContext& context, const AssetItemPtr& item) { auto& projectManager = context.GetProjectManager(); - const int itemIndex = FindProjectItemIndex(projectManager, item); + const bool hasTarget = item != nullptr && !item->fullPath.empty(); DrawMenuAction(MakeOpenAssetAction(Commands::CanOpenAsset(item)), [&]() { Commands::OpenAsset(context, item); }); DrawMenuSeparator(); - DrawMenuAction(MakeDeleteAssetAction(itemIndex >= 0), [&]() { - Commands::DeleteAsset(projectManager, itemIndex); + DrawMenuAction(MakeDeleteAssetAction(hasTarget), [&]() { + Commands::DeleteAsset(projectManager, item); }); } diff --git a/editor/src/Application.cpp b/editor/src/Application.cpp index c5513f82..a601fc8f 100644 --- a/editor/src/Application.cpp +++ b/editor/src/Application.cpp @@ -5,6 +5,7 @@ #include "Core/EditorContext.h" #include "Core/EditorEvents.h" #include "Core/EventBus.h" +#include "UI/BuiltInIcons.h" #include "Platform/Win32Utf8.h" #include "Platform/WindowsProcessDiagnostics.h" #include @@ -18,7 +19,20 @@ Application& Application::Get() { } bool Application::InitializeWindowRenderer(HWND hwnd) { - if (m_windowRenderer.Initialize(hwnd, 1280, 720)) { + RECT clientRect = {}; + if (!GetClientRect(hwnd, &clientRect)) { + MessageBoxW(hwnd, L"Failed to query editor client area", L"Error", MB_OK | MB_ICONERROR); + return false; + } + + const int clientWidth = clientRect.right - clientRect.left; + const int clientHeight = clientRect.bottom - clientRect.top; + if (clientWidth <= 0 || clientHeight <= 0) { + MessageBoxW(hwnd, L"Editor client area is invalid", L"Error", MB_OK | MB_ICONERROR); + return false; + } + + if (m_windowRenderer.Initialize(hwnd, clientWidth, clientHeight)) { return true; } @@ -38,8 +52,20 @@ void Application::InitializeEditorContext(const std::string& projectPath) { } void Application::InitializeImGui(HWND hwnd) { - m_imguiSession.Initialize(m_editorContext->GetProjectPath()); - m_imguiBackend.Initialize(hwnd, m_windowRenderer.GetDevice(), m_windowRenderer.GetSrvHeap()); + m_imguiSession.Initialize( + m_editorContext->GetProjectPath(), + UI::ImGuiBackendBridge::GetDpiScaleForHwnd(hwnd)); + m_imguiBackend.Initialize( + hwnd, + m_windowRenderer.GetDevice(), + m_windowRenderer.GetCommandQueue(), + m_windowRenderer.GetSrvHeap(), + m_windowRenderer.GetSrvDescriptorSize(), + m_windowRenderer.GetSrvDescriptorCount()); + UI::InitializeBuiltInIcons( + m_imguiBackend, + m_windowRenderer.GetDevice(), + m_windowRenderer.GetCommandQueue()); } void Application::AttachEditorLayer() { @@ -65,7 +91,6 @@ void Application::ShutdownEditorContext() { void Application::RenderEditorFrame() { static constexpr float kClearColor[4] = { 0.22f, 0.22f, 0.22f, 1.0f }; - m_imguiBackend.BeginFrame(); m_layerStack.onImGuiRender(); UpdateWindowTitle(); @@ -89,11 +114,14 @@ bool Application::Initialize(HWND hwnd) { InitializeEditorContext(exeDir); InitializeImGui(hwnd); AttachEditorLayer(); + m_renderReady = true; return true; } void Application::Shutdown() { + m_renderReady = false; DetachEditorLayer(); + UI::ShutdownBuiltInIcons(); m_imguiBackend.Shutdown(); m_imguiSession.Shutdown(); ShutdownEditorContext(); @@ -101,6 +129,9 @@ void Application::Shutdown() { } void Application::Render() { + if (!m_renderReady) { + return; + } RenderEditorFrame(); } diff --git a/editor/src/Application.h b/editor/src/Application.h index ce5e47d7..b80245a3 100644 --- a/editor/src/Application.h +++ b/editor/src/Application.h @@ -24,6 +24,7 @@ public: void Shutdown(); void Render(); void OnResize(int width, int height); + bool IsRenderReady() const { return m_renderReady; } HWND GetWindowHandle() const { return m_hwnd; } IEditorContext& GetEditorContext() const { return *m_editorContext; } @@ -50,6 +51,7 @@ private: UI::ImGuiSession m_imguiSession; uint64_t m_exitRequestedHandlerId = 0; std::wstring m_lastWindowTitle; + bool m_renderReady = false; }; } diff --git a/editor/src/Commands/ProjectCommands.h b/editor/src/Commands/ProjectCommands.h index 00860962..4a9c47bd 100644 --- a/editor/src/Commands/ProjectCommands.h +++ b/editor/src/Commands/ProjectCommands.h @@ -37,13 +37,20 @@ inline bool CreateFolder(IProjectManager& projectManager, const std::string& nam return true; } -inline bool DeleteAsset(IProjectManager& projectManager, int index) { - if (index < 0) { +inline bool DeleteAsset(IProjectManager& projectManager, const std::string& fullPath) { + if (fullPath.empty()) { return false; } - projectManager.DeleteItem(index); - return true; + return projectManager.DeleteItem(fullPath); +} + +inline bool DeleteAsset(IProjectManager& projectManager, const AssetItemPtr& item) { + if (!item) { + return false; + } + + return DeleteAsset(projectManager, item->fullPath); } inline bool MoveAssetToFolder( diff --git a/editor/src/Core/IProjectManager.h b/editor/src/Core/IProjectManager.h index 8c30eb39..d967e85b 100644 --- a/editor/src/Core/IProjectManager.h +++ b/editor/src/Core/IProjectManager.h @@ -12,9 +12,16 @@ class IProjectManager { public: virtual ~IProjectManager() = default; - virtual std::vector& GetCurrentItems() = 0; + virtual const std::vector& GetCurrentItems() const = 0; + virtual AssetItemPtr GetRootFolder() const = 0; + virtual AssetItemPtr GetCurrentFolder() const = 0; + virtual AssetItemPtr GetSelectedItem() const = 0; + virtual const std::string& GetSelectedItemPath() const = 0; virtual int GetSelectedIndex() const = 0; virtual void SetSelectedIndex(int index) = 0; + virtual void SetSelectedItem(const AssetItemPtr& item) = 0; + virtual void ClearSelection() = 0; + virtual int FindCurrentItemIndex(const std::string& fullPath) const = 0; virtual void NavigateToFolder(const AssetItemPtr& folder) = 0; virtual void NavigateBack() = 0; @@ -29,11 +36,11 @@ public: virtual void RefreshCurrentFolder() = 0; virtual void CreateFolder(const std::string& name) = 0; - virtual void DeleteItem(int index) = 0; + virtual bool DeleteItem(const std::string& fullPath) = 0; virtual bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) = 0; virtual const std::string& GetProjectPath() const = 0; }; } // namespace Editor -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/editor/src/EditorApp.rc b/editor/src/EditorApp.rc new file mode 100644 index 00000000..00fa9b07 --- /dev/null +++ b/editor/src/EditorApp.rc @@ -0,0 +1,3 @@ +#include "EditorResources.h" + +IDI_APP_ICON ICON "../resources/Icons/app.ico" diff --git a/editor/src/EditorResources.h b/editor/src/EditorResources.h new file mode 100644 index 00000000..c17a17a1 --- /dev/null +++ b/editor/src/EditorResources.h @@ -0,0 +1,3 @@ +#pragma once + +#define IDI_APP_ICON 101 diff --git a/editor/src/Layout/DockLayoutController.h b/editor/src/Layout/DockLayoutController.h index fc2be440..680726a9 100644 --- a/editor/src/Layout/DockLayoutController.h +++ b/editor/src/Layout/DockLayoutController.h @@ -4,6 +4,7 @@ #include "Core/EventBus.h" #include "Core/IEditorContext.h" #include "UI/DockHostStyle.h" +#include "UI/DockTabBarChrome.h" #include #include @@ -57,6 +58,7 @@ public: ImGui::PopStyleVar(2); const ImGuiID dockspaceId = ImGui::GetID("MainDockspace.Root"); + UI::ConfigureDockTabBarChrome(dockspaceId); { UI::DockHostStyleScope dockHostStyle; ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), m_dockspaceFlags); @@ -66,6 +68,7 @@ public: BuildDefaultLayout(dockspaceId, viewport->Size); m_layoutDirty = false; } + UI::ConfigureDockTabBarChrome(dockspaceId); ImGui::End(); } diff --git a/editor/src/Managers/ProjectManager.cpp b/editor/src/Managers/ProjectManager.cpp index b91416ef..a8ff7d0b 100644 --- a/editor/src/Managers/ProjectManager.cpp +++ b/editor/src/Managers/ProjectManager.cpp @@ -1,6 +1,7 @@ #include "ProjectManager.h" #include #include +#include #include #include @@ -9,23 +10,104 @@ namespace fs = std::filesystem; namespace XCEngine { namespace Editor { -std::vector& ProjectManager::GetCurrentItems() { +namespace { + +std::wstring MakePathKey(const fs::path& path) { + std::wstring key = path.lexically_normal().generic_wstring(); + std::transform(key.begin(), key.end(), key.begin(), ::towlower); + return key; +} + +bool IsSameOrDescendantPath(const fs::path& path, const fs::path& ancestor) { + const std::wstring pathKey = MakePathKey(path); + std::wstring ancestorKey = MakePathKey(ancestor); + if (pathKey.empty() || ancestorKey.empty()) { + return false; + } + if (pathKey == ancestorKey) { + return true; + } + + if (ancestorKey.back() != L'/') { + ancestorKey += L'/'; + } + return pathKey.rfind(ancestorKey, 0) == 0; +} + +} // namespace + +const std::vector& ProjectManager::GetCurrentItems() const { if (m_path.empty()) { - static std::vector empty; + static const std::vector empty; return empty; } return m_path.back()->children; } +AssetItemPtr ProjectManager::GetSelectedItem() const { + return FindCurrentItemByPath(m_selectedItemPath); +} + +int ProjectManager::GetSelectedIndex() const { + return FindCurrentItemIndex(m_selectedItemPath); +} + +void ProjectManager::SetSelectedIndex(int index) { + const auto& items = GetCurrentItems(); + if (index < 0 || index >= static_cast(items.size())) { + ClearSelection(); + return; + } + + SetSelectedItem(items[index]); +} + +void ProjectManager::SetSelectedItem(const AssetItemPtr& item) { + if (!item) { + ClearSelection(); + return; + } + + m_selectedItemPath = item->fullPath; +} + +void ProjectManager::ClearSelection() { + m_selectedItemPath.clear(); +} + +int ProjectManager::FindCurrentItemIndex(const std::string& fullPath) const { + if (fullPath.empty()) { + return -1; + } + + const auto& items = GetCurrentItems(); + for (int i = 0; i < static_cast(items.size()); ++i) { + if (items[i] && items[i]->fullPath == fullPath) { + return i; + } + } + + return -1; +} + void ProjectManager::NavigateToFolder(const AssetItemPtr& folder) { - m_path.push_back(folder); - m_selectedIndex = -1; + if (!folder || !folder->isFolder || !m_rootFolder) { + return; + } + + std::vector resolvedPath; + if (!BuildPathToFolder(m_rootFolder, folder->fullPath, resolvedPath)) { + return; + } + + m_path = std::move(resolvedPath); + ClearSelection(); } void ProjectManager::NavigateBack() { if (m_path.size() > 1) { m_path.pop_back(); - m_selectedIndex = -1; + ClearSelection(); } } @@ -34,7 +116,7 @@ void ProjectManager::NavigateToIndex(size_t index) { while (m_path.size() > index + 1) { m_path.pop_back(); } - m_selectedIndex = -1; + ClearSelection(); } std::string ProjectManager::GetCurrentPath() const { @@ -79,16 +161,7 @@ void ProjectManager::Initialize(const std::string& projectPath) { try { if (!fs::exists(assetsPath)) { fs::create_directories(assetsPath); - fs::create_directories(assetsPath / L"Textures"); - fs::create_directories(assetsPath / L"Models"); - fs::create_directories(assetsPath / L"Scripts"); - fs::create_directories(assetsPath / L"Materials"); fs::create_directories(assetsPath / L"Scenes"); - - std::ofstream((assetsPath / L"Textures" / L"Grass.png").wstring()); - std::ofstream((assetsPath / L"Textures" / L"Stone.png").wstring()); - std::ofstream((assetsPath / L"Models" / L"Character.fbx").wstring()); - std::ofstream((assetsPath / L"Scripts" / L"PlayerController.cs").wstring()); } m_rootFolder = ScanDirectory(assetsPath.wstring()); @@ -97,32 +170,32 @@ void ProjectManager::Initialize(const std::string& projectPath) { m_path.clear(); m_path.push_back(m_rootFolder); - m_selectedIndex = -1; - } catch (const std::exception& e) { + ClearSelection(); + } catch (...) { m_rootFolder = std::make_shared(); m_rootFolder->name = "Assets"; m_rootFolder->isFolder = true; m_rootFolder->type = "Folder"; + m_rootFolder->fullPath = WstringToUtf8(assetsPath.wstring()); + m_path.clear(); m_path.push_back(m_rootFolder); + ClearSelection(); } } std::wstring ProjectManager::GetCurrentFullPathW() const { - if (m_path.empty()) return Utf8ToWstring(m_projectPath); - - std::wstring fullPath = Utf8ToWstring(m_projectPath); - for (size_t i = 0; i < m_path.size(); i++) { - fullPath += L"/" + Utf8ToWstring(m_path[i]->name); + if (AssetItemPtr currentFolder = GetCurrentFolder()) { + return Utf8ToWstring(currentFolder->fullPath); } - return fullPath; + + return Utf8ToWstring(m_projectPath); } void ProjectManager::RefreshCurrentFolder() { if (m_path.empty()) return; try { - auto newFolder = ScanDirectory(GetCurrentFullPathW()); - m_path.back()->children = newFolder->children; + RebuildTreePreservingPath(); } catch (...) { } } @@ -137,34 +210,60 @@ void ProjectManager::CreateFolder(const std::string& name) { } } -void ProjectManager::DeleteItem(int index) { - if (m_path.empty()) return; - auto& items = m_path.back()->children; - if (index < 0 || index >= (int)items.size()) return; - +bool ProjectManager::DeleteItem(const std::string& fullPath) { + if (fullPath.empty() || !m_rootFolder) { + return false; + } + try { - std::wstring fullPath = GetCurrentFullPathW(); - fs::path itemPath = fs::path(fullPath) / Utf8ToWstring(items[index]->name); + const fs::path itemPath = Utf8ToWstring(fullPath); + const fs::path rootPath = Utf8ToWstring(m_rootFolder->fullPath); + if (!fs::exists(itemPath) || !IsSameOrDescendantPath(itemPath, rootPath)) { + return false; + } + if (MakePathKey(itemPath) == MakePathKey(rootPath)) { + return false; + } + fs::remove_all(itemPath); - m_selectedIndex = -1; + if (m_selectedItemPath == fullPath) { + ClearSelection(); + } RefreshCurrentFolder(); + return true; } catch (...) { + return false; } } bool ProjectManager::MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) { + if (sourceFullPath.empty() || destFolderFullPath.empty() || !m_rootFolder) { + return false; + } + try { - fs::path sourcePath = Utf8ToWstring(sourceFullPath); - fs::path destPath = fs::path(Utf8ToWstring(destFolderFullPath)) / sourcePath.filename(); - - if (!fs::exists(sourcePath)) { + const fs::path sourcePath = Utf8ToWstring(sourceFullPath); + const fs::path destFolderPath = Utf8ToWstring(destFolderFullPath); + const fs::path rootPath = Utf8ToWstring(m_rootFolder->fullPath); + + if (!fs::exists(sourcePath) || !fs::exists(destFolderPath) || !fs::is_directory(destFolderPath)) { return false; } - - if (fs::exists(destPath)) { + if (!IsSameOrDescendantPath(sourcePath, rootPath) || !IsSameOrDescendantPath(destFolderPath, rootPath)) { return false; } - + if (MakePathKey(sourcePath) == MakePathKey(rootPath)) { + return false; + } + if (fs::is_directory(sourcePath) && IsSameOrDescendantPath(destFolderPath, sourcePath)) { + return false; + } + + const fs::path destPath = destFolderPath / sourcePath.filename(); + if (MakePathKey(destPath) == MakePathKey(sourcePath) || fs::exists(destPath)) { + return false; + } + fs::rename(sourcePath, destPath); RefreshCurrentFolder(); return true; @@ -173,11 +272,27 @@ bool ProjectManager::MoveItem(const std::string& sourceFullPath, const std::stri } } +AssetItemPtr ProjectManager::FindCurrentItemByPath(const std::string& fullPath) const { + const int index = FindCurrentItemIndex(fullPath); + if (index < 0) { + return nullptr; + } + + return GetCurrentItems()[index]; +} + +void ProjectManager::SyncSelection() { + if (!m_selectedItemPath.empty() && !FindCurrentItemByPath(m_selectedItemPath)) { + ClearSelection(); + } +} + AssetItemPtr ProjectManager::ScanDirectory(const std::wstring& path) { auto folder = std::make_shared(); folder->name = WstringToUtf8(fs::path(path).filename().wstring()); folder->isFolder = true; folder->type = "Folder"; + folder->fullPath = WstringToUtf8(path); if (!fs::exists(path)) return folder; @@ -201,6 +316,61 @@ AssetItemPtr ProjectManager::ScanDirectory(const std::wstring& path) { return folder; } +bool ProjectManager::BuildPathToFolder( + const AssetItemPtr& current, + const std::string& fullPath, + std::vector& outPath) const { + if (!current || !current->isFolder) { + return false; + } + + outPath.push_back(current); + if (current->fullPath == fullPath) { + return true; + } + + for (const auto& child : current->children) { + if (!child || !child->isFolder) { + continue; + } + + if (BuildPathToFolder(child, fullPath, outPath)) { + return true; + } + } + + outPath.pop_back(); + return false; +} + +void ProjectManager::RebuildTreePreservingPath() { + std::vector preservedPaths; + preservedPaths.reserve(m_path.size()); + for (const auto& folder : m_path) { + if (folder && folder->isFolder) { + preservedPaths.push_back(folder->fullPath); + } + } + + const fs::path assetsPath = fs::path(Utf8ToWstring(m_projectPath)) / L"Assets"; + m_rootFolder = ScanDirectory(assetsPath.wstring()); + m_rootFolder->name = "Assets"; + m_rootFolder->fullPath = WstringToUtf8(assetsPath.wstring()); + + m_path.clear(); + m_path.push_back(m_rootFolder); + + for (size_t i = 1; i < preservedPaths.size(); ++i) { + std::vector resolvedPath; + if (!BuildPathToFolder(m_rootFolder, preservedPaths[i], resolvedPath)) { + break; + } + m_path = std::move(resolvedPath); + } + + SyncSelection(); +} + AssetItemPtr ProjectManager::CreateAssetItem(const std::wstring& path, const std::wstring& nameW, bool isFolder) { auto item = std::make_shared(); item->name = WstringToUtf8(nameW); diff --git a/editor/src/Managers/ProjectManager.h b/editor/src/Managers/ProjectManager.h index b6a0da6c..39e4dc84 100644 --- a/editor/src/Managers/ProjectManager.h +++ b/editor/src/Managers/ProjectManager.h @@ -11,9 +11,16 @@ namespace Editor { class ProjectManager : public IProjectManager { public: - std::vector& GetCurrentItems() override; - int GetSelectedIndex() const override { return m_selectedIndex; } - void SetSelectedIndex(int index) override { m_selectedIndex = index; } + const std::vector& GetCurrentItems() const override; + AssetItemPtr GetRootFolder() const override { return m_rootFolder; } + AssetItemPtr GetCurrentFolder() const override { return m_path.empty() ? nullptr : m_path.back(); } + AssetItemPtr GetSelectedItem() const override; + const std::string& GetSelectedItemPath() const override { return m_selectedItemPath; } + int GetSelectedIndex() const override; + void SetSelectedIndex(int index) override; + void SetSelectedItem(const AssetItemPtr& item) override; + void ClearSelection() override; + int FindCurrentItemIndex(const std::string& fullPath) const override; void NavigateToFolder(const AssetItemPtr& folder) override; void NavigateBack() override; @@ -28,21 +35,25 @@ public: void RefreshCurrentFolder() override; void CreateFolder(const std::string& name) override; - void DeleteItem(int index) override; + bool DeleteItem(const std::string& fullPath) override; bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) override; const std::string& GetProjectPath() const override { return m_projectPath; } private: + bool BuildPathToFolder(const AssetItemPtr& current, const std::string& fullPath, std::vector& outPath) const; + void RebuildTreePreservingPath(); + AssetItemPtr FindCurrentItemByPath(const std::string& fullPath) const; + void SyncSelection(); AssetItemPtr ScanDirectory(const std::wstring& path); AssetItemPtr CreateAssetItem(const std::wstring& path, const std::wstring& nameW, bool isFolder); std::wstring GetCurrentFullPathW() const; AssetItemPtr m_rootFolder; std::vector m_path; - int m_selectedIndex = -1; + std::string m_selectedItemPath; std::string m_projectPath; }; } -} \ No newline at end of file +} diff --git a/editor/src/Platform/D3D12WindowRenderer.h b/editor/src/Platform/D3D12WindowRenderer.h index 5ea3813c..4964c434 100644 --- a/editor/src/Platform/D3D12WindowRenderer.h +++ b/editor/src/Platform/D3D12WindowRenderer.h @@ -4,6 +4,7 @@ #include #include +#include #include namespace XCEngine { @@ -12,6 +13,8 @@ namespace Platform { class D3D12WindowRenderer { public: + static constexpr UINT kSrvDescriptorCount = 64; + bool Initialize(HWND hwnd, int width, int height) { m_hwnd = hwnd; m_width = width; @@ -36,7 +39,9 @@ public: m_height = 720; m_fenceValue = 0; m_rtvDescriptorSize = 0; + m_srvDescriptorSize = 0; m_frameIndex = 0; + m_srvDescriptorUsage.fill(false); } void Resize(int width, int height) { @@ -107,6 +112,18 @@ public: return m_srvHeap; } + ID3D12CommandQueue* GetCommandQueue() const { + return m_commandQueue; + } + + UINT GetSrvDescriptorSize() const { + return m_srvDescriptorSize; + } + + UINT GetSrvDescriptorCount() const { + return kSrvDescriptorCount; + } + private: bool CreateDevice() { HRESULT hr = D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&m_device)); @@ -164,10 +181,12 @@ private: D3D12_DESCRIPTOR_HEAP_DESC srvDesc = {}; srvDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; - srvDesc.NumDescriptors = 1; + srvDesc.NumDescriptors = kSrvDescriptorCount; srvDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; hr = m_device->CreateDescriptorHeap(&srvDesc, IID_PPV_ARGS(&m_srvHeap)); if (FAILED(hr)) return false; + m_srvDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + m_srvDescriptorUsage.fill(false); return true; } @@ -210,7 +229,9 @@ private: ID3D12Fence* m_fence = nullptr; UINT64 m_fenceValue = 0; UINT m_rtvDescriptorSize = 0; + UINT m_srvDescriptorSize = 0; UINT m_frameIndex = 0; + std::array m_srvDescriptorUsage = {}; }; } // namespace Platform diff --git a/editor/src/Platform/Win32EditorHost.h b/editor/src/Platform/Win32EditorHost.h index 16fa44d8..2046ee41 100644 --- a/editor/src/Platform/Win32EditorHost.h +++ b/editor/src/Platform/Win32EditorHost.h @@ -1,6 +1,7 @@ #pragma once #include "Application.h" +#include "EditorResources.h" #include "UI/ImGuiBackendBridge.h" #include @@ -18,8 +19,22 @@ inline LRESULT WINAPI EditorWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM l case WM_SIZE: if (wParam != SIZE_MINIMIZED) { Application::Get().OnResize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + if (Application::Get().IsRenderReady()) { + Application::Get().Render(); + } } return 0; + case WM_PAINT: + if (Application::Get().IsRenderReady()) { + PAINTSTRUCT ps = {}; + BeginPaint(hWnd, &ps); + Application::Get().Render(); + EndPaint(hWnd, &ps); + return 0; + } + break; + case WM_ERASEBKGND: + return 1; case WM_SYSCOMMAND: if ((wParam & 0xfff0) == SC_KEYMENU) { return 0; @@ -34,11 +49,21 @@ inline LRESULT WINAPI EditorWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM l } inline int RunEditor(HINSTANCE hInstance, int nCmdShow) { + UI::ImGuiBackendBridge::EnableDpiAwareness(); + WNDCLASSEXW wc = {}; wc.cbSize = sizeof(wc); wc.style = CS_CLASSDC; wc.lpfnWndProc = EditorWndProc; wc.hInstance = hInstance; + wc.hIcon = static_cast(LoadImageW(hInstance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE)); + wc.hIconSm = static_cast(LoadImageW( + hInstance, + MAKEINTRESOURCEW(IDI_APP_ICON), + IMAGE_ICON, + GetSystemMetrics(SM_CXSMICON), + GetSystemMetrics(SM_CYSMICON), + LR_DEFAULTCOLOR)); wc.lpszClassName = L"XCVolumeRendererUI2"; if (!RegisterClassExW(&wc)) { @@ -64,6 +89,13 @@ inline int RunEditor(HINSTANCE hInstance, int nCmdShow) { return 1; } + if (wc.hIcon) { + SendMessageW(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(wc.hIcon)); + } + if (wc.hIconSm) { + SendMessageW(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(wc.hIconSm)); + } + ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); diff --git a/editor/src/UI/BuiltInIcons.cpp b/editor/src/UI/BuiltInIcons.cpp new file mode 100644 index 00000000..b40056f0 --- /dev/null +++ b/editor/src/UI/BuiltInIcons.cpp @@ -0,0 +1,375 @@ +#include "BuiltInIcons.h" + +#include "ImGuiBackendBridge.h" +#include "Platform/Win32Utf8.h" +#include "StyleTokens.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace XCEngine { +namespace Editor { +namespace UI { + +namespace { + +using Microsoft::WRL::ComPtr; + +struct BuiltInTexture { + ImTextureID textureId = {}; + D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle = {}; + D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = {}; + ComPtr texture; + int width = 0; + int height = 0; + + bool IsValid() const { + return textureId != ImTextureID{} && texture != nullptr && width > 0 && height > 0; + } +}; + +struct BuiltInIconState { + ImGuiBackendBridge* backend = nullptr; + BuiltInTexture folder; + BuiltInTexture gameObject; +}; + +BuiltInIconState g_icons; + +std::filesystem::path ResolveFolderIconPath() { + const std::filesystem::path exeDir(Platform::GetExecutableDirectoryUtf8()); + return (exeDir / ".." / ".." / "resources" / "Icons" / "folder_icon.png").lexically_normal(); +} + +std::filesystem::path ResolveGameObjectIconPath() { + const std::filesystem::path exeDir(Platform::GetExecutableDirectoryUtf8()); + return (exeDir / ".." / ".." / "resources" / "Icons" / "gameobject_icon.png").lexically_normal(); +} + +void ResetTexture(BuiltInTexture& texture) { + if (g_icons.backend && texture.cpuHandle.ptr != 0) { + g_icons.backend->FreeTextureDescriptor(texture.cpuHandle, texture.gpuHandle); + } + + texture.texture.Reset(); + texture.textureId = {}; + texture.cpuHandle = {}; + texture.gpuHandle = {}; + texture.width = 0; + texture.height = 0; +} + +bool WaitForQueueIdle(ID3D12Device* device, ID3D12CommandQueue* commandQueue) { + if (!device || !commandQueue) { + return false; + } + + ComPtr fence; + if (FAILED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)))) { + return false; + } + + HANDLE eventHandle = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!eventHandle) { + return false; + } + + constexpr UINT64 kFenceValue = 1; + const HRESULT signalHr = commandQueue->Signal(fence.Get(), kFenceValue); + if (FAILED(signalHr)) { + CloseHandle(eventHandle); + return false; + } + + if (fence->GetCompletedValue() < kFenceValue) { + if (FAILED(fence->SetEventOnCompletion(kFenceValue, eventHandle))) { + CloseHandle(eventHandle); + return false; + } + WaitForSingleObject(eventHandle, INFINITE); + } + + CloseHandle(eventHandle); + return true; +} + +bool LoadTextureFromFile( + ImGuiBackendBridge& backend, + ID3D12Device* device, + ID3D12CommandQueue* commandQueue, + const std::filesystem::path& filePath, + BuiltInTexture& outTexture) { + if (!device || !commandQueue || !std::filesystem::exists(filePath)) { + return false; + } + + int width = 0; + int height = 0; + int channels = 0; + stbi_uc* pixels = stbi_load(filePath.string().c_str(), &width, &height, &channels, STBI_rgb_alpha); + if (!pixels || width <= 0 || height <= 0) { + if (pixels) { + stbi_image_free(pixels); + } + return false; + } + + const UINT srcRowPitch = static_cast(width * 4); + + D3D12_RESOURCE_DESC textureDesc = {}; + textureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + textureDesc.Alignment = 0; + textureDesc.Width = static_cast(width); + textureDesc.Height = static_cast(height); + textureDesc.DepthOrArraySize = 1; + textureDesc.MipLevels = 1; + textureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + textureDesc.SampleDesc.Count = 1; + textureDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + + D3D12_HEAP_PROPERTIES defaultHeap = {}; + defaultHeap.Type = D3D12_HEAP_TYPE_DEFAULT; + + ComPtr textureResource; + if (FAILED(device->CreateCommittedResource( + &defaultHeap, + D3D12_HEAP_FLAG_NONE, + &textureDesc, + D3D12_RESOURCE_STATE_COPY_DEST, + nullptr, + IID_PPV_ARGS(&textureResource)))) { + stbi_image_free(pixels); + return false; + } + + D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint = {}; + UINT numRows = 0; + UINT64 rowSizeInBytes = 0; + UINT64 uploadBufferSize = 0; + device->GetCopyableFootprints(&textureDesc, 0, 1, 0, &footprint, &numRows, &rowSizeInBytes, &uploadBufferSize); + + D3D12_RESOURCE_DESC uploadDesc = {}; + uploadDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + uploadDesc.Width = uploadBufferSize; + uploadDesc.Height = 1; + uploadDesc.DepthOrArraySize = 1; + uploadDesc.MipLevels = 1; + uploadDesc.SampleDesc.Count = 1; + uploadDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + + D3D12_HEAP_PROPERTIES uploadHeap = {}; + uploadHeap.Type = D3D12_HEAP_TYPE_UPLOAD; + + ComPtr uploadResource; + if (FAILED(device->CreateCommittedResource( + &uploadHeap, + D3D12_HEAP_FLAG_NONE, + &uploadDesc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(&uploadResource)))) { + stbi_image_free(pixels); + return false; + } + + std::uint8_t* mappedData = nullptr; + if (FAILED(uploadResource->Map(0, nullptr, reinterpret_cast(&mappedData)))) { + stbi_image_free(pixels); + return false; + } + + for (UINT row = 0; row < numRows; ++row) { + std::memcpy( + mappedData + footprint.Offset + static_cast(row) * footprint.Footprint.RowPitch, + pixels + static_cast(row) * srcRowPitch, + srcRowPitch); + } + uploadResource->Unmap(0, nullptr); + stbi_image_free(pixels); + + ComPtr commandAllocator; + ComPtr commandList; + if (FAILED(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)))) { + return false; + } + if (FAILED(device->CreateCommandList( + 0, + D3D12_COMMAND_LIST_TYPE_DIRECT, + commandAllocator.Get(), + nullptr, + IID_PPV_ARGS(&commandList)))) { + return false; + } + + D3D12_TEXTURE_COPY_LOCATION dst = {}; + dst.pResource = textureResource.Get(); + dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dst.SubresourceIndex = 0; + + D3D12_TEXTURE_COPY_LOCATION src = {}; + src.pResource = uploadResource.Get(); + src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + src.PlacedFootprint = footprint; + + commandList->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr); + + D3D12_RESOURCE_BARRIER barrier = {}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Transition.pResource = textureResource.Get(); + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + commandList->ResourceBarrier(1, &barrier); + + if (FAILED(commandList->Close())) { + return false; + } + + ID3D12CommandList* commandLists[] = { commandList.Get() }; + commandQueue->ExecuteCommandLists(1, commandLists); + + if (!WaitForQueueIdle(device, commandQueue)) { + return false; + } + + backend.AllocateTextureDescriptor(&outTexture.cpuHandle, &outTexture.gpuHandle); + + D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + device->CreateShaderResourceView(textureResource.Get(), &srvDesc, outTexture.cpuHandle); + + outTexture.texture = textureResource; + outTexture.textureId = (ImTextureID)(static_cast(outTexture.gpuHandle.ptr)); + outTexture.width = width; + outTexture.height = height; + return true; +} + +ImVec2 ComputeFittedIconSize(const BuiltInTexture& texture, const ImVec2& min, const ImVec2& max) { + const float availableWidth = max.x - min.x; + const float availableHeight = max.y - min.y; + if (availableWidth <= 0.0f || availableHeight <= 0.0f || texture.width <= 0 || texture.height <= 0) { + return ImVec2(0.0f, 0.0f); + } + + const float scale = (std::min)( + availableWidth / static_cast(texture.width), + availableHeight / static_cast(texture.height)); + return ImVec2( + static_cast(texture.width) * scale, + static_cast(texture.height) * scale); +} + +void DrawTextureIcon(ImDrawList* drawList, const BuiltInTexture& texture, const ImVec2& min, const ImVec2& max) { + if (!drawList || !texture.IsValid()) { + return; + } + + const ImVec2 size = ComputeFittedIconSize(texture, min, max); + const float x = min.x + ((max.x - min.x) - size.x) * 0.5f; + const float y = min.y + ((max.y - min.y) - size.y) * 0.5f; + drawList->AddImage(texture.textureId, ImVec2(x, y), ImVec2(x + size.x, y + size.y)); +} + +void DrawBuiltInFolderFallback(ImDrawList* drawList, const ImVec2& min, const ImVec2& max) { + if (!drawList) { + return; + } + + const float width = max.x - min.x; + const float height = max.y - min.y; + if (width <= 0.0f || height <= 0.0f) { + return; + } + + const float rounding = (std::max)(1.0f, (std::min)(width, height) * 0.18f); + const ImU32 tabColor = ImGui::GetColorU32(BuiltInFolderIconTabColor()); + const ImU32 topColor = ImGui::GetColorU32(BuiltInFolderIconTopColor()); + const ImU32 bodyColor = ImGui::GetColorU32(BuiltInFolderIconBodyColor()); + + const ImVec2 tabMin(min.x + width * 0.08f, min.y + height * 0.14f); + const ImVec2 tabMax(min.x + width * 0.48f, min.y + height * 0.38f); + const ImVec2 topMin(min.x + width * 0.24f, min.y + height * 0.22f); + const ImVec2 topMax(min.x + width * 0.90f, min.y + height * 0.42f); + const ImVec2 bodyMin(min.x + width * 0.06f, min.y + height * 0.32f); + const ImVec2 bodyMax(min.x + width * 0.94f, min.y + height * 0.88f); + + drawList->AddRectFilled(tabMin, tabMax, tabColor, rounding); + drawList->AddRectFilled( + topMin, + topMax, + topColor, + rounding, + ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight); + drawList->AddRectFilled(bodyMin, bodyMax, bodyColor, rounding); +} + +void DrawBuiltInFileIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max) { + if (!drawList) { + return; + } + + const ImU32 fillColor = ImGui::GetColorU32(AssetFileIconFillColor()); + const ImU32 lineColor = ImGui::GetColorU32(AssetFileIconLineColor()); + const ImVec2 foldA(max.x - 8.0f, min.y); + const ImVec2 foldB(max.x, min.y + 8.0f); + drawList->AddRectFilled(min, max, fillColor, 2.0f); + drawList->AddRect(min, max, lineColor, 2.0f); + drawList->AddTriangleFilled(foldA, ImVec2(max.x, min.y), foldB, ImGui::GetColorU32(AssetFileFoldColor())); + drawList->AddLine(foldA, foldB, lineColor); +} + +} // namespace + +void InitializeBuiltInIcons( + ImGuiBackendBridge& backend, + ID3D12Device* device, + ID3D12CommandQueue* commandQueue) { + ShutdownBuiltInIcons(); + g_icons.backend = &backend; + LoadTextureFromFile(backend, device, commandQueue, ResolveFolderIconPath(), g_icons.folder); + LoadTextureFromFile(backend, device, commandQueue, ResolveGameObjectIconPath(), g_icons.gameObject); +} + +void ShutdownBuiltInIcons() { + ResetTexture(g_icons.folder); + ResetTexture(g_icons.gameObject); + g_icons.backend = nullptr; +} + +void DrawAssetIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, AssetIconKind kind) { + if (kind == AssetIconKind::Folder) { + if (g_icons.folder.IsValid()) { + DrawTextureIcon(drawList, g_icons.folder, min, max); + return; + } + + DrawBuiltInFolderFallback(drawList, min, max); + return; + } + + if (kind == AssetIconKind::GameObject) { + if (g_icons.gameObject.IsValid()) { + DrawTextureIcon(drawList, g_icons.gameObject, min, max); + return; + } + + DrawBuiltInFileIcon(drawList, min, max); + return; + } + + DrawBuiltInFileIcon(drawList, min, max); +} + +} // namespace UI +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/UI/BuiltInIcons.h b/editor/src/UI/BuiltInIcons.h new file mode 100644 index 00000000..c6f515de --- /dev/null +++ b/editor/src/UI/BuiltInIcons.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +struct ID3D12Device; +struct ID3D12CommandQueue; + +namespace XCEngine { +namespace Editor { +namespace UI { + +class ImGuiBackendBridge; + +enum class AssetIconKind { + Folder, + File, + GameObject +}; + +void InitializeBuiltInIcons( + ImGuiBackendBridge& backend, + ID3D12Device* device, + ID3D12CommandQueue* commandQueue); + +void ShutdownBuiltInIcons(); + +void DrawAssetIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, AssetIconKind kind); + +} // namespace UI +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/UI/Core.h b/editor/src/UI/Core.h index 51d6d4a2..82dab6f3 100644 --- a/editor/src/UI/Core.h +++ b/editor/src/UI/Core.h @@ -9,54 +9,6 @@ namespace XCEngine { namespace Editor { namespace UI { -inline float DefaultControlLabelWidth() { - return InspectorPropertyLabelWidth(); -} - -inline ImVec2 DefaultControlCellPadding() { - return ControlCellPadding(); -} - -inline ImVec2 DefaultControlFramePadding() { - return ControlFramePadding(); -} - -inline void PushControlRowStyles() { - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, DefaultControlCellPadding()); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, DefaultControlFramePadding()); -} - -template -inline auto DrawControlRow( - const char* label, - float columnWidth, - DrawControlFn&& drawControl) -> decltype(drawControl()) { - using Result = decltype(drawControl()); - - Result result{}; - ImGui::PushID(label); - PushControlRowStyles(); - - if (ImGui::BeginTable("##ControlRow", 2, ImGuiTableFlags_NoSavedSettings)) { - ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); - ImGui::TableSetupColumn("##control", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + ControlRowHeightOffset()); - - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(label); - - ImGui::TableNextColumn(); - result = drawControl(); - - ImGui::EndTable(); - } - - ImGui::PopStyleVar(2); - ImGui::PopID(); - return result; -} - inline void StyleVarPush(ImGuiStyleVar idx, float val) { ImGui::PushStyleVar(idx, val); } @@ -81,33 +33,158 @@ inline void PopStyleColor(int count = 1) { ImGui::PopStyleColor(count); } -inline void PushPopupWindowStyle() { +inline void DrawDisclosureArrow(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, bool open, ImU32 color) { + if (!drawList) { + return; + } + + const ImVec2 center((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f); + const float width = max.x - min.x; + const float height = max.y - min.y; + const float size = (width < height ? width : height) * DisclosureArrowScale(); + if (size <= 0.0f) { + return; + } + + if (open) { + drawList->AddTriangleFilled( + ImVec2(center.x - size, center.y - size * 0.45f), + ImVec2(center.x + size, center.y - size * 0.45f), + ImVec2(center.x, center.y + size), + color); + return; + } + + drawList->AddTriangleFilled( + ImVec2(center.x - size * 0.45f, center.y - size), + ImVec2(center.x - size * 0.45f, center.y + size), + ImVec2(center.x + size, center.y), + color); +} + +inline constexpr int PopupWindowChromeVarCount() { + return 3; +} + +inline constexpr int PopupWindowChromeColorCount() { + return 2; +} + +inline constexpr int PopupContentChromeVarCount() { + return 0; +} + +inline constexpr int PopupContentChromeColorCount() { + return 7; +} + +inline constexpr int ComboPopupWindowChromeVarCount() { + return 3; +} + +inline constexpr int ComboPopupWindowChromeColorCount() { + return 2; +} + +inline constexpr int ComboPopupContentChromeVarCount() { + return 1; +} + +inline constexpr int ComboPopupContentChromeColorCount() { + return 5; +} + +inline void PushPopupWindowChrome() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, PopupWindowPadding()); + ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, PopupWindowRounding()); + ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, PopupWindowBorderSize()); + ImGui::PushStyleColor(ImGuiCol_PopupBg, PopupBackgroundColor()); + ImGui::PushStyleColor(ImGuiCol_Border, PopupBorderColor()); +} + +inline void PopPopupWindowChrome() { + ImGui::PopStyleColor(PopupWindowChromeColorCount()); + ImGui::PopStyleVar(PopupWindowChromeVarCount()); +} + +inline void PushComboPopupWindowChrome() { + const ImVec4 borderColor = ImGui::GetStyleColorVec4(ImGuiCol_Border); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ComboPopupWindowPadding()); + ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, ComboPopupRounding()); + ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, ComboPopupBorderSize()); + ImGui::PushStyleColor(ImGuiCol_PopupBg, ComboPopupBackgroundColor()); + ImGui::PushStyleColor(ImGuiCol_Border, borderColor); +} + +inline void PopComboPopupWindowChrome() { + ImGui::PopStyleColor(ComboPopupWindowChromeColorCount()); + ImGui::PopStyleVar(ComboPopupWindowChromeVarCount()); +} + +inline void PushPopupContentChrome() { + ImGui::PushStyleColor(ImGuiCol_Text, PopupTextColor()); + ImGui::PushStyleColor(ImGuiCol_TextDisabled, PopupTextDisabledColor()); + ImGui::PushStyleColor(ImGuiCol_Header, PopupItemColor()); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, PopupItemHoveredColor()); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, PopupItemActiveColor()); + ImGui::PushStyleColor(ImGuiCol_Separator, PopupBorderColor()); + ImGui::PushStyleColor(ImGuiCol_CheckMark, PopupCheckMarkColor()); +} + +inline void PopPopupContentChrome() { + ImGui::PopStyleColor(PopupContentChromeColorCount()); + if (PopupContentChromeVarCount() > 0) { + ImGui::PopStyleVar(PopupContentChromeVarCount()); + } +} + +inline void PushComboPopupContentChrome() { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ComboPopupItemSpacing()); + ImGui::PushStyleColor(ImGuiCol_Text, ComboPopupTextColor()); + ImGui::PushStyleColor(ImGuiCol_TextDisabled, ComboPopupTextDisabledColor()); + ImGui::PushStyleColor(ImGuiCol_Header, ComboPopupItemColor()); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ComboPopupItemHoveredColor()); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ComboPopupItemActiveColor()); +} + +inline void PopComboPopupContentChrome() { + ImGui::PopStyleColor(ComboPopupContentChromeColorCount()); + ImGui::PopStyleVar(ComboPopupContentChromeVarCount()); +} + +inline void PushPopupChromeStyle() { + PushPopupWindowChrome(); + PushPopupContentChrome(); +} + +inline void PopPopupChromeStyle() { + PopPopupContentChrome(); + PopPopupWindowChrome(); } inline bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0) { - PushPopupWindowStyle(); + PushPopupChromeStyle(); bool is_open = ImGui::BeginPopup(str_id, flags); if (!is_open) { - ImGui::PopStyleVar(); + PopPopupChromeStyle(); } return is_open; } inline bool BeginPopupContextItem(const char* str_id = nullptr, ImGuiPopupFlags popup_flags = ImGuiPopupFlags_MouseButtonRight) { - PushPopupWindowStyle(); + PushPopupChromeStyle(); bool is_open = ImGui::BeginPopupContextItem(str_id, popup_flags); if (!is_open) { - ImGui::PopStyleVar(); + PopPopupChromeStyle(); } return is_open; } inline bool BeginPopupContextWindow(const char* str_id = nullptr, ImGuiPopupFlags popup_flags = ImGuiPopupFlags_MouseButtonRight) { - PushPopupWindowStyle(); + PushPopupChromeStyle(); bool is_open = ImGui::BeginPopupContextWindow(str_id, popup_flags); if (!is_open) { - ImGui::PopStyleVar(); + PopPopupChromeStyle(); } return is_open; } @@ -116,17 +193,60 @@ inline bool BeginModalPopup( const char* name, bool* p_open = nullptr, ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize) { - PushPopupWindowStyle(); + PushPopupChromeStyle(); bool is_open = ImGui::BeginPopupModal(name, p_open, flags); if (!is_open) { - ImGui::PopStyleVar(); + PopPopupChromeStyle(); } return is_open; } inline void EndPopup() { ImGui::EndPopup(); - ImGui::PopStyleVar(); + PopPopupChromeStyle(); +} + +inline bool BeginStyledCombo( + const char* label, + const char* preview_value, + ImGuiComboFlags flags = ImGuiComboFlags_None) { + const ImVec4 previewBorderColor = ImGui::GetStyleColorVec4(ImGuiCol_Border); + PushComboPopupWindowChrome(); + ImGui::PushStyleColor(ImGuiCol_Border, previewBorderColor); + const bool is_open = ImGui::BeginCombo(label, preview_value, flags); + if (!is_open) { + ImGui::PopStyleColor(); + PopComboPopupWindowChrome(); + return false; + } + + ImGui::PopStyleColor(); + PopComboPopupWindowChrome(); + PushComboPopupContentChrome(); + return true; +} + +inline void EndStyledCombo() { + PopComboPopupContentChrome(); + ImGui::EndCombo(); +} + +inline bool BeginStyledComboPopup(const char* str_id, ImGuiWindowFlags flags = ImGuiWindowFlags_None) { + PushComboPopupWindowChrome(); + const bool is_open = ImGui::BeginPopup(str_id, flags); + if (!is_open) { + PopComboPopupWindowChrome(); + return false; + } + + PopComboPopupWindowChrome(); + PushComboPopupContentChrome(); + return true; +} + +inline void EndStyledComboPopup() { + PopComboPopupContentChrome(); + ImGui::EndPopup(); } inline void BeginDisabled(bool disabled = true) { diff --git a/editor/src/UI/DockTabBarChrome.h b/editor/src/UI/DockTabBarChrome.h index f8bcd653..8dc7891c 100644 --- a/editor/src/UI/DockTabBarChrome.h +++ b/editor/src/UI/DockTabBarChrome.h @@ -219,16 +219,169 @@ inline ImU32 ResolveCustomDockOverlineColor(const ImGuiDockNode& node, const ImG node.IsFocused ? ImGuiWindowDockStyleCol_TabSelectedOverline : ImGuiWindowDockStyleCol_TabDimmedSelectedOverline]; } +inline ImGuiID ResolveCustomDockTabItemId(const ImGuiWindow& window) { + return window.TabId != 0 ? window.TabId : window.MoveId; +} + +inline void UpdateDockWindowDisplayOrder(ImGuiDockNode& node) { + auto& order = DockTabOrderCache()[node.ID]; + int dockOrder = 0; + for (ImGuiID tabId : order) { + if (ImGuiWindow* window = FindDockWindowByTabId(node, tabId)) { + window->DockOrder = dockOrder++; + } + } + + for (ImGuiWindow* window : node.Windows) { + if (!window) { + continue; + } + if (std::find(order.begin(), order.end(), window->TabId) == order.end()) { + window->DockOrder = dockOrder++; + } + } +} + +inline void ReorderDockTab(ImGuiDockNode& node, ImGuiID tabId, int destinationIndex) { + SyncDockTabOrderCache(node); + + auto& order = DockTabOrderCache()[node.ID]; + const auto it = std::find(order.begin(), order.end(), tabId); + if (it == order.end()) { + return; + } + + const int sourceIndex = static_cast(std::distance(order.begin(), it)); + destinationIndex = std::clamp(destinationIndex, 0, static_cast(order.size()) - 1); + if (destinationIndex == sourceIndex) { + return; + } + + order.erase(it); + order.insert(order.begin() + destinationIndex, tabId); + UpdateDockWindowDisplayOrder(node); + ImGui::MarkIniSettingsDirty(); +} + +inline void UpdateDraggedDockTabOrder( + ImGuiDockNode& node, + const std::vector& tabRects, + ImGuiID draggedTabId, + int sourceIndex) { + if (sourceIndex < 0 || sourceIndex >= static_cast(tabRects.size())) { + return; + } + + const ImRect& sourceRect = tabRects[sourceIndex]; + const float mouseX = ImGui::GetIO().MousePos.x; + if (mouseX >= sourceRect.Min.x && mouseX <= sourceRect.Max.x) { + return; + } + + int destinationIndex = static_cast(tabRects.size()) - 1; + for (int i = 0; i < static_cast(tabRects.size()); ++i) { + const float centerX = (tabRects[i].Min.x + tabRects[i].Max.x) * 0.5f; + if (mouseX < centerX) { + destinationIndex = i; + break; + } + } + + if (destinationIndex > sourceIndex) { + --destinationIndex; + } + + ReorderDockTab(node, draggedTabId, destinationIndex); +} + +inline bool ShouldUndockDraggedDockTab( + const ImRect& tabRect, + int sourceIndex, + int tabCount) { + const ImGuiIO& io = ImGui::GetIO(); + const float thresholdBase = ImGui::GetFontSize(); + const float thresholdX = thresholdBase * 2.2f; + const float thresholdY = + (thresholdBase * 1.5f) + + ImClamp((ImFabs(io.MouseDragMaxDistanceAbs[0].x) - thresholdBase * 2.0f) * 0.20f, 0.0f, thresholdBase * 4.0f); + + const float distanceFromEdgeY = ImMax(tabRect.Min.y - io.MousePos.y, io.MousePos.y - tabRect.Max.y); + if (distanceFromEdgeY >= thresholdY) { + return true; + } + + const bool draggingLeft = io.MousePos.x < tabRect.Min.x; + const bool draggingRight = io.MousePos.x > tabRect.Max.x; + if (draggingLeft && sourceIndex == 0 && (tabRect.Min.x - io.MousePos.x) > thresholdX) { + return true; + } + if (draggingRight && sourceIndex == tabCount - 1 && (io.MousePos.x - tabRect.Max.x) > thresholdX) { + return true; + } + + return false; +} + +inline void BeginCustomDockTabUndock(ImGuiWindow& targetWindow, const ImRect& tabRect) { + ImGuiContext& g = *GImGui; + ImGui::DockContextQueueUndockWindow(&g, &targetWindow); + g.MovingWindow = &targetWindow; + ImGui::SetActiveID(targetWindow.MoveId, &targetWindow); + g.ActiveIdClickOffset.x -= (targetWindow.Pos.x - tabRect.Min.x); + g.ActiveIdClickOffset.y -= (targetWindow.Pos.y - tabRect.Min.y); + g.ActiveIdNoClearOnFocusLoss = true; + ImGui::SetActiveIdUsingAllKeyboardKeys(); +} + inline void DrawCustomDockTab( ImGuiDockNode& node, ImGuiWindow& targetWindow, const ImRect& tabRect, - const char* idSuffix) { + const std::vector& tabRects, + int tabIndex) { + ImGuiContext& g = *GImGui; + const ImGuiID itemId = ResolveCustomDockTabItemId(targetWindow); ImGui::SetCursorScreenPos(tabRect.Min); - ImGui::InvisibleButton(idSuffix, tabRect.GetSize()); + ImGui::ItemSize(tabRect.GetSize(), 0.0f); + const bool submitted = ImGui::ItemAdd(tabRect, itemId); + bool hovered = false; + bool held = false; + bool clicked = false; + if (submitted) { + clicked = ImGui::ButtonBehavior( + tabRect, + itemId, + &hovered, + &held, + ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_AllowOverlap); + } + + if (held && g.ActiveId == itemId && g.ActiveIdIsJustActivated) { + g.ActiveIdWindow = &targetWindow; + } + + const ImGuiDockNode* dockNode = targetWindow.DockNode; + const bool singleFloatingWindowNode = dockNode && dockNode->IsFloatingNode() && dockNode->Windows.Size == 1; + if (held && singleFloatingWindowNode && ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.0f)) { + ImGui::StartMouseMovingWindow(&targetWindow); + } else if (held && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { + const bool canUndock = + dockNode && + (targetWindow.Flags & ImGuiWindowFlags_NoMove) == 0 && + (dockNode->MergedFlags & ImGuiDockNodeFlags_NoUndocking) == 0; + if (canUndock && ShouldUndockDraggedDockTab(tabRect, tabIndex, static_cast(tabRects.size()))) { + BeginCustomDockTabUndock(targetWindow, tabRect); + } else if (!g.DragDropActive) { + const ImGuiID draggedTabId = targetWindow.TabId; + auto& order = DockTabOrderCache()[node.ID]; + const auto sourceIt = std::find(order.begin(), order.end(), draggedTabId); + if (sourceIt != order.end()) { + const int sourceIndex = static_cast(std::distance(order.begin(), sourceIt)); + UpdateDraggedDockTabOrder(node, tabRects, draggedTabId, sourceIndex); + } + } + } - const bool hovered = ImGui::IsItemHovered(); - const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); ImDrawList* drawList = ImGui::GetWindowDrawList(); ImRect fillRect = tabRect; if (IsDockWindowSelected(node, targetWindow)) { @@ -300,11 +453,10 @@ inline void DrawDockedWindowTabStrip() { 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; + std::vector tabRects; + tabRects.reserve(orderedWindows.size()); + float cursorX = stripMin.x; for (ImGuiWindow* dockedWindow : orderedWindows) { if (!dockedWindow) { continue; @@ -313,19 +465,27 @@ inline void DrawDockedWindowTabStrip() { 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(); + tabRects.emplace_back(cursorX, stripMin.y, cursorX + tabWidth, stripMin.y + stripHeight); + cursorX += tabWidth; + } + + float selectedTabMinX = stripMin.x; + float selectedTabMaxX = stripMin.x; + bool hasSelectedTab = false; + for (int i = 0; i < static_cast(orderedWindows.size()) && i < static_cast(tabRects.size()); ++i) { + ImGuiWindow* dockedWindow = orderedWindows[i]; + if (!dockedWindow) { + continue; + } + + const ImRect& tabRect = tabRects[i]; + DrawCustomDockTab(node, *dockedWindow, tabRect, tabRects, i); if (IsDockWindowSelected(node, *dockedWindow)) { selectedTabMinX = tabRect.Min.x; selectedTabMaxX = tabRect.Max.x; hasSelectedTab = true; } - - cursorX += tabWidth; } const float dividerY = stripMax.y - 0.5f; diff --git a/editor/src/UI/ImGuiBackendBridge.h b/editor/src/UI/ImGuiBackendBridge.h index 583ac281..12171f7e 100644 --- a/editor/src/UI/ImGuiBackendBridge.h +++ b/editor/src/UI/ImGuiBackendBridge.h @@ -1,12 +1,25 @@ #pragma once +#ifndef NOMINMAX +#define NOMINMAX +#endif + #include #include #include #include #include +#include #include +#ifdef min +#undef min +#endif + +#ifdef max +#undef max +#endif + extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); namespace XCEngine { @@ -15,20 +28,42 @@ namespace UI { class ImGuiBackendBridge { public: + static void EnableDpiAwareness() { + ImGui_ImplWin32_EnableDpiAwareness(); + } + + static float GetDpiScaleForHwnd(HWND hwnd) { + return hwnd ? ImGui_ImplWin32_GetDpiScaleForHwnd(hwnd) : 1.0f; + } + void Initialize( HWND hwnd, ID3D12Device* device, + ID3D12CommandQueue* commandQueue, ID3D12DescriptorHeap* srvHeap, + UINT srvDescriptorSize, + UINT srvDescriptorCount, int frameCount = 3, DXGI_FORMAT backBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM) { + m_srvHeap = srvHeap; + m_srvDescriptorSize = srvDescriptorSize; + m_srvCpuStart = srvHeap->GetCPUDescriptorHandleForHeapStart(); + m_srvGpuStart = srvHeap->GetGPUDescriptorHandleForHeapStart(); + m_srvUsage.assign(srvDescriptorCount, false); + ImGui_ImplWin32_Init(hwnd); - ImGui_ImplDX12_Init( - device, - frameCount, - backBufferFormat, - srvHeap, - srvHeap->GetCPUDescriptorHandleForHeapStart(), - srvHeap->GetGPUDescriptorHandleForHeapStart()); + + ImGui_ImplDX12_InitInfo initInfo = {}; + initInfo.Device = device; + initInfo.CommandQueue = commandQueue; + initInfo.NumFramesInFlight = frameCount; + initInfo.RTVFormat = backBufferFormat; + initInfo.DSVFormat = DXGI_FORMAT_UNKNOWN; + initInfo.UserData = this; + initInfo.SrvDescriptorHeap = srvHeap; + initInfo.SrvDescriptorAllocFn = &ImGuiBackendBridge::AllocateSrvDescriptor; + initInfo.SrvDescriptorFreeFn = &ImGuiBackendBridge::FreeSrvDescriptor; + ImGui_ImplDX12_Init(&initInfo); m_initialized = true; } @@ -39,6 +74,11 @@ public: ImGui_ImplDX12_Shutdown(); ImGui_ImplWin32_Shutdown(); + m_srvUsage.clear(); + m_srvHeap = nullptr; + m_srvDescriptorSize = 0; + m_srvCpuStart.ptr = 0; + m_srvGpuStart.ptr = 0; m_initialized = false; } @@ -52,12 +92,82 @@ public: ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), commandList); } + void AllocateTextureDescriptor( + D3D12_CPU_DESCRIPTOR_HANDLE* outCpuHandle, + D3D12_GPU_DESCRIPTOR_HANDLE* outGpuHandle) { + AllocateSrvDescriptorInternal(outCpuHandle, outGpuHandle); + } + + void FreeTextureDescriptor( + D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle, + D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle) { + FreeSrvDescriptorInternal(cpuHandle, gpuHandle); + } + static bool HandleWindowMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { return ImGui_ImplWin32_WndProcHandler(hwnd, msg, wParam, lParam) != 0; } private: + static void AllocateSrvDescriptor( + ImGui_ImplDX12_InitInfo* info, + D3D12_CPU_DESCRIPTOR_HANDLE* outCpuHandle, + D3D12_GPU_DESCRIPTOR_HANDLE* outGpuHandle) { + ImGuiBackendBridge* bridge = static_cast(info->UserData); + IM_ASSERT(bridge != nullptr); + bridge->AllocateSrvDescriptorInternal(outCpuHandle, outGpuHandle); + } + + static void FreeSrvDescriptor( + ImGui_ImplDX12_InitInfo* info, + D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle, + D3D12_GPU_DESCRIPTOR_HANDLE) { + ImGuiBackendBridge* bridge = static_cast(info->UserData); + IM_ASSERT(bridge != nullptr); + bridge->FreeSrvDescriptorInternal(cpuHandle, {}); + } + + void AllocateSrvDescriptorInternal( + D3D12_CPU_DESCRIPTOR_HANDLE* outCpuHandle, + D3D12_GPU_DESCRIPTOR_HANDLE* outGpuHandle) { + IM_ASSERT(outCpuHandle != nullptr && outGpuHandle != nullptr); + + for (size_t i = 0; i < m_srvUsage.size(); ++i) { + if (m_srvUsage[i]) { + continue; + } + + m_srvUsage[i] = true; + outCpuHandle->ptr = m_srvCpuStart.ptr + static_cast(i) * m_srvDescriptorSize; + outGpuHandle->ptr = m_srvGpuStart.ptr + static_cast(i) * m_srvDescriptorSize; + return; + } + + IM_ASSERT(false && "ImGui SRV descriptor heap is exhausted."); + *outCpuHandle = m_srvCpuStart; + *outGpuHandle = m_srvGpuStart; + } + + void FreeSrvDescriptorInternal( + D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle, + D3D12_GPU_DESCRIPTOR_HANDLE) { + if (m_srvDescriptorSize == 0 || cpuHandle.ptr < m_srvCpuStart.ptr) { + return; + } + + const SIZE_T offset = cpuHandle.ptr - m_srvCpuStart.ptr; + const size_t index = static_cast(offset / m_srvDescriptorSize); + if (index < m_srvUsage.size()) { + m_srvUsage[index] = false; + } + } + bool m_initialized = false; + ID3D12DescriptorHeap* m_srvHeap = nullptr; + UINT m_srvDescriptorSize = 0; + D3D12_CPU_DESCRIPTOR_HANDLE m_srvCpuStart = {}; + D3D12_GPU_DESCRIPTOR_HANDLE m_srvGpuStart = {}; + std::vector m_srvUsage; }; } // namespace UI diff --git a/editor/src/UI/ImGuiSession.h b/editor/src/UI/ImGuiSession.h index 8bec7776..1e5833ae 100644 --- a/editor/src/UI/ImGuiSession.h +++ b/editor/src/UI/ImGuiSession.h @@ -13,16 +13,18 @@ namespace UI { class ImGuiSession { public: - void Initialize(const std::string& projectPath) { + void Initialize(const std::string& projectPath, float mainDpiScale = 1.0f) { IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + io.ConfigDpiScaleFonts = true; + io.ConfigDpiScaleViewports = true; ConfigureIniFile(projectPath, io); + ConfigureStyle(ImGui::GetStyle(), mainDpiScale); ConfigureFonts(io); - ApplyBaseTheme(ImGui::GetStyle()); } void Shutdown() { @@ -47,6 +49,10 @@ public: } private: + static constexpr float kUiFontSize = 18.0f; + static constexpr const char* kPrimaryUiFontPath = "C:/Windows/Fonts/segoeui.ttf"; + static constexpr const char* kChineseFallbackFontPath = "C:/Windows/Fonts/msyh.ttc"; + void ConfigureIniFile(const std::string& projectPath, ImGuiIO& io) { const std::filesystem::path configDir = std::filesystem::path(projectPath) / ".xceditor"; std::error_code ec; @@ -56,17 +62,54 @@ private: io.IniFilename = m_iniPath.c_str(); } + void ConfigureStyle(ImGuiStyle& style, float mainDpiScale) const { + ApplyBaseTheme(style); + + const float dpiScale = mainDpiScale < 1.0f ? 1.0f : (mainDpiScale > 4.0f ? 4.0f : mainDpiScale); + style.ScaleAllSizes(dpiScale); + style.FontScaleDpi = dpiScale; + } + void ConfigureFonts(ImGuiIO& io) const { - if (ImFont* uiFont = io.Fonts->AddFontFromFileTTF("C:/Windows/Fonts/msyh.ttc", 15.0f)) { - io.FontDefault = uiFont; + ImFontAtlas* atlas = io.Fonts; + atlas->Clear(); + + ImFontConfig baseConfig; + baseConfig.OversampleH = 2; + baseConfig.OversampleV = 1; + baseConfig.PixelSnapH = true; + + ImFont* uiFont = atlas->AddFontFromFileTTF( + kPrimaryUiFontPath, + kUiFontSize, + &baseConfig, + atlas->GetGlyphRangesDefault()); + + if (uiFont) { + ImFontConfig mergeConfig = baseConfig; + mergeConfig.MergeMode = true; + mergeConfig.PixelSnapH = true; + atlas->AddFontFromFileTTF( + kChineseFallbackFontPath, + kUiFontSize, + &mergeConfig, + atlas->GetGlyphRangesChineseSimplifiedCommon()); } else { - io.FontDefault = io.Fonts->AddFontDefault(); + uiFont = atlas->AddFontFromFileTTF( + kChineseFallbackFontPath, + kUiFontSize, + &baseConfig, + atlas->GetGlyphRangesChineseSimplifiedCommon()); } - unsigned char* pixels = nullptr; - int width = 0; - int height = 0; - io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + if (!uiFont) { + ImFontConfig fallbackConfig = baseConfig; + fallbackConfig.SizePixels = kUiFontSize; + uiFont = atlas->AddFontDefault(&fallbackConfig); + } + + io.FontDefault = uiFont; + atlas->Build(); } std::string m_iniPath; diff --git a/editor/src/UI/PanelChrome.h b/editor/src/UI/PanelChrome.h index 564647c4..f72191da 100644 --- a/editor/src/UI/PanelChrome.h +++ b/editor/src/UI/PanelChrome.h @@ -1,6 +1,7 @@ #pragma once #include "Core.h" +#include "DockTabBarChrome.h" #include "StyleTokens.h" #include @@ -9,12 +10,30 @@ namespace XCEngine { namespace Editor { namespace UI { +inline void CollapsePanelSectionSpacing() { + const float spacingY = ImGui::GetStyle().ItemSpacing.y; + if (spacingY <= 0.0f) { + return; + } + + const float cursorY = ImGui::GetCursorPosY(); + const float startY = ImGui::GetCursorStartPos().y; + if (cursorY <= startY) { + return; + } + + ImGui::SetCursorPosY(cursorY - spacingY); +} + class PanelWindowScope { public: explicit PanelWindowScope(const char* name, ImGuiWindowFlags flags = ImGuiWindowFlags_None) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, PanelWindowPadding()); m_open = ImGui::Begin(name, nullptr, flags); ImGui::PopStyleVar(); + if (m_open) { + DrawDockedWindowTabStrip(); + } m_began = true; } @@ -44,9 +63,11 @@ public: ImGuiWindowFlags flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse, bool drawBottomBorder = true, ImVec2 padding = ToolbarPadding(), - ImVec2 itemSpacing = ToolbarItemSpacing()) + ImVec2 itemSpacing = ToolbarItemSpacing(), + ImVec4 backgroundColor = ToolbarBackgroundColor()) : m_drawBottomBorder(drawBottomBorder) { - ImGui::PushStyleColor(ImGuiCol_ChildBg, ToolbarBackgroundColor()); + CollapsePanelSectionSpacing(); + ImGui::PushStyleColor(ImGuiCol_ChildBg, backgroundColor); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, itemSpacing); m_open = ImGui::BeginChild(id, ImVec2(0.0f, height), false, flags); @@ -82,16 +103,18 @@ public: explicit PanelContentScope( const char* id, ImVec2 padding = DefaultPanelContentPadding(), + ImGuiChildFlags childFlags = ImGuiChildFlags_None, ImGuiWindowFlags flags = ImGuiWindowFlags_None, bool pushItemSpacing = false, ImVec2 itemSpacing = ImVec2(0.0f, 0.0f)) { + CollapsePanelSectionSpacing(); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding); m_styleVarCount = 1; if (pushItemSpacing) { ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, itemSpacing); ++m_styleVarCount; } - m_open = ImGui::BeginChild(id, ImVec2(0.0f, 0.0f), false, flags); + m_open = ImGui::BeginChild(id, ImVec2(0.0f, 0.0f), childFlags, flags); m_began = true; } diff --git a/editor/src/UI/PropertyGrid.h b/editor/src/UI/PropertyGrid.h index 77c47485..0de8b973 100644 --- a/editor/src/UI/PropertyGrid.h +++ b/editor/src/UI/PropertyGrid.h @@ -9,6 +9,10 @@ namespace XCEngine { namespace Editor { namespace UI { +inline PropertyLayoutSpec InspectorPropertyLayout() { + return MakePropertyLayout(); +} + template inline bool ApplyPropertyChange( bool changedByWidget, @@ -35,7 +39,7 @@ inline bool DrawPropertyFloat( float max = 0.0f, const char* format = "%.2f" ) { - return DrawFloat(label, value, InspectorPropertyLabelWidth(), dragSpeed, min, max, format); + return DrawFloat(label, value, InspectorPropertyLayout(), dragSpeed, min, max, format); } inline bool DrawPropertyInt( @@ -45,28 +49,28 @@ inline bool DrawPropertyInt( int min = 0, int max = 0 ) { - return DrawInt(label, value, InspectorPropertyLabelWidth(), step, min, max); + return DrawInt(label, value, InspectorPropertyLayout(), step, min, max); } inline bool DrawPropertyBool( const char* label, bool& value ) { - return DrawBool(label, value, InspectorPropertyLabelWidth()); + return DrawBool(label, value, InspectorPropertyLayout()); } inline bool DrawPropertyColor3( const char* label, float color[3] ) { - return DrawColor3(label, color, InspectorPropertyLabelWidth()); + return DrawColor3(label, color, InspectorPropertyLayout()); } inline bool DrawPropertyColor4( const char* label, float color[4] ) { - return DrawColor4(label, color, InspectorPropertyLabelWidth()); + return DrawColor4(label, color, InspectorPropertyLayout()); } inline bool DrawPropertySliderFloat( @@ -76,7 +80,7 @@ inline bool DrawPropertySliderFloat( float max, const char* format = "%.2f" ) { - return DrawSliderFloat(label, value, min, max, InspectorPropertyLabelWidth(), format); + return DrawSliderFloat(label, value, min, max, InspectorPropertyLayout(), format); } inline bool DrawPropertySliderInt( @@ -85,7 +89,7 @@ inline bool DrawPropertySliderInt( int min, int max ) { - return DrawSliderInt(label, value, min, max, InspectorPropertyLabelWidth()); + return DrawSliderInt(label, value, min, max, InspectorPropertyLayout()); } inline int DrawPropertyCombo( @@ -95,7 +99,7 @@ inline int DrawPropertyCombo( int itemCount, int heightInItems = -1 ) { - return DrawCombo(label, currentItem, items, itemCount, InspectorPropertyLabelWidth(), heightInItems); + return DrawCombo(label, currentItem, items, itemCount, InspectorPropertyLayout(), heightInItems); } inline bool DrawPropertyVec2( @@ -104,7 +108,7 @@ inline bool DrawPropertyVec2( float resetValue = 0.0f, float dragSpeed = 0.1f ) { - return DrawVec2(label, values, resetValue, InspectorPropertyLabelWidth(), dragSpeed); + return DrawVec2(label, values, resetValue, InspectorPropertyLayout(), dragSpeed); } inline bool DrawPropertyVec3( @@ -114,7 +118,7 @@ inline bool DrawPropertyVec3( float dragSpeed = 0.1f, bool* isActive = nullptr ) { - return DrawVec3(label, values, resetValue, InspectorPropertyLabelWidth(), dragSpeed, isActive); + return DrawVec3(label, values, resetValue, InspectorPropertyLayout(), dragSpeed, isActive); } inline bool DrawPropertyVec3Input( @@ -123,7 +127,7 @@ inline bool DrawPropertyVec3Input( float dragSpeed = 0.1f, bool* isActive = nullptr ) { - return DrawVec3Input(label, values, InspectorPropertyLabelWidth(), dragSpeed, isActive); + return DrawVec3Input(label, values, InspectorPropertyLayout(), dragSpeed, isActive); } } diff --git a/editor/src/UI/PropertyLayout.h b/editor/src/UI/PropertyLayout.h new file mode 100644 index 00000000..bae55e4f --- /dev/null +++ b/editor/src/UI/PropertyLayout.h @@ -0,0 +1,128 @@ +#pragma once + +#include "StyleTokens.h" + +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace UI { + +struct PropertyLayoutSpec { + float labelInset = InspectorPropertyLabelInset(); + float controlColumnStart = InspectorPropertyControlColumnStart(); + float labelControlGap = InspectorPropertyLabelControlGap(); + float controlTrailingInset = InspectorPropertyControlTrailingInset(); +}; + +struct PropertyLayoutMetrics { + ImVec2 cursorPos = ImVec2(0.0f, 0.0f); + ImVec2 screenPos = ImVec2(0.0f, 0.0f); + float rowWidth = 0.0f; + float rowHeight = 0.0f; + float labelX = 0.0f; + float labelWidth = 0.0f; + float controlX = 0.0f; + float controlWidth = 0.0f; +}; + +inline PropertyLayoutSpec MakePropertyLayout() { + return PropertyLayoutSpec{}; +} + +inline void PushPropertyLayoutStyles() { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ControlFramePadding()); +} + +inline float GetPropertyControlWidth( + const PropertyLayoutMetrics& layout, + float trailingInset = 0.0f) { + return std::max(layout.controlWidth - trailingInset, 1.0f); +} + +inline void SetNextPropertyControlWidth( + const PropertyLayoutMetrics& layout, + float trailingInset = 0.0f) { + ImGui::SetNextItemWidth(GetPropertyControlWidth(layout, trailingInset)); +} + +inline void AlignPropertyControlToRight( + const PropertyLayoutMetrics& layout, + float width, + float trailingInset = 0.0f) { + const float offset = layout.controlWidth - width - trailingInset; + if (offset > 0.0f) { + ImGui::SetCursorPosX(layout.controlX + offset); + } +} + +template +inline auto DrawPropertyRow( + const char* label, + const PropertyLayoutSpec& spec, + DrawControlFn&& drawControl) -> decltype(drawControl(std::declval())) { + using Result = decltype(drawControl(std::declval())); + + Result result{}; + ImGui::PushID(label); + PushPropertyLayoutStyles(); + + const ImVec2 rowCursorPos = ImGui::GetCursorPos(); + const ImVec2 rowScreenPos = ImGui::GetCursorScreenPos(); + const float rowWidth = std::max(ImGui::GetContentRegionAvail().x, 1.0f); + const float rowHeight = ImGui::GetFrameHeight() + ControlRowHeightOffset(); + const float labelInset = std::max(spec.labelInset, 0.0f); + const float controlColumnStart = std::clamp( + std::max(spec.controlColumnStart, 0.0f), + labelInset, + rowWidth); + const float labelWidth = std::max( + controlColumnStart - labelInset - std::max(spec.labelControlGap, 0.0f), + 0.0f); + const float controlX = rowCursorPos.x + controlColumnStart; + const float controlRight = rowCursorPos.x + rowWidth - std::max(spec.controlTrailingInset, 0.0f); + const float controlWidth = std::max(controlRight - controlX, 1.0f); + const float labelX = rowCursorPos.x + labelInset; + + const PropertyLayoutMetrics metrics{ + rowCursorPos, + rowScreenPos, + rowWidth, + rowHeight, + labelX, + labelWidth, + controlX, + controlWidth + }; + + if (label && label[0] != '\0') { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const float textY = rowScreenPos.y + std::max(0.0f, (rowHeight - ImGui::GetTextLineHeight()) * 0.5f); + drawList->PushClipRect( + ImVec2(rowScreenPos.x + labelInset, rowScreenPos.y), + ImVec2(rowScreenPos.x + labelInset + labelWidth, rowScreenPos.y + rowHeight), + true); + drawList->AddText( + ImVec2(rowScreenPos.x + labelInset, textY), + ImGui::GetColorU32(ImGuiCol_Text), + label); + drawList->PopClipRect(); + } + + ImGui::SetCursorPos(ImVec2(controlX, rowCursorPos.y)); + result = drawControl(metrics); + + const float consumedHeight = std::max(rowHeight, ImGui::GetCursorPosY() - rowCursorPos.y); + ImGui::SetCursorPos(rowCursorPos); + ImGui::Dummy(ImVec2(rowWidth, consumedHeight)); + + ImGui::PopStyleVar(); + ImGui::PopID(); + return result; +} + +} // namespace UI +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/UI/ScalarControls.h b/editor/src/UI/ScalarControls.h index cf4eb16f..e432f109 100644 --- a/editor/src/UI/ScalarControls.h +++ b/editor/src/UI/ScalarControls.h @@ -1,35 +1,165 @@ #pragma once #include "Core.h" +#include "PropertyLayout.h" + +#include +#include #include namespace XCEngine { namespace Editor { namespace UI { +inline float CalcComboPopupMaxHeightFromItemCount(int itemCount) { + if (itemCount <= 0) { + return FLT_MAX; + } + + const ImGuiStyle& style = ImGui::GetStyle(); + return (ImGui::GetFontSize() + style.ItemSpacing.y) * itemCount - + style.ItemSpacing.y + + (ComboPopupWindowPadding().y * 2.0f); +} + +inline void DrawComboPreviewFrame( + const char* id, + const char* previewValue, + float width, + bool popupOpen) { + const float frameHeight = ImGui::GetFrameHeight(); + ImGui::InvisibleButton(id, ImVec2(width, frameHeight)); + + const bool hovered = ImGui::IsItemHovered(); + const ImVec2 min = ImGui::GetItemRectMin(); + const ImVec2 max = ImGui::GetItemRectMax(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const ImGuiStyle& style = ImGui::GetStyle(); + + const float arrowWidth = frameHeight; + const float valueMaxX = ImMax(min.x, max.x - arrowWidth); + const ImU32 frameColor = ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + const ImU32 arrowColor = ImGui::GetColorU32((popupOpen || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + const ImU32 borderColor = ImGui::GetColorU32(ImGuiCol_Border); + const ImU32 textColor = ImGui::GetColorU32(ImGuiCol_Text); + + drawList->AddRectFilled( + min, + ImVec2(valueMaxX, max.y), + frameColor, + style.FrameRounding, + ImDrawFlags_RoundCornersLeft); + drawList->AddRectFilled( + ImVec2(valueMaxX, min.y), + max, + arrowColor, + style.FrameRounding, + ImDrawFlags_RoundCornersRight); + drawList->AddRect( + ImVec2(min.x + 0.5f, min.y + 0.5f), + ImVec2(max.x - 0.5f, max.y - 0.5f), + borderColor, + style.FrameRounding, + 0, + style.FrameBorderSize); + + if (previewValue && previewValue[0] != '\0') { + const ImVec2 textSize = ImGui::CalcTextSize(previewValue); + const ImVec2 textPos( + min.x + style.FramePadding.x, + min.y + ImMax(0.0f, (frameHeight - textSize.y) * 0.5f)); + ImGui::PushClipRect( + ImVec2(min.x + style.FramePadding.x, min.y), + ImVec2(valueMaxX - style.FramePadding.x, max.y), + true); + drawList->AddText(textPos, textColor, previewValue); + ImGui::PopClipRect(); + } + + const float arrowHalfWidth = 4.0f; + const float arrowHalfHeight = 2.5f; + const ImVec2 arrowCenter((valueMaxX + max.x) * 0.5f, (min.y + max.y) * 0.5f + 0.5f); + drawList->AddTriangleFilled( + ImVec2(arrowCenter.x - arrowHalfWidth, arrowCenter.y - arrowHalfHeight), + ImVec2(arrowCenter.x + arrowHalfWidth, arrowCenter.y - arrowHalfHeight), + ImVec2(arrowCenter.x, arrowCenter.y + arrowHalfHeight), + textColor); +} + inline bool DrawFloat( const char* label, float& value, - float columnWidth = DefaultControlLabelWidth(), + const PropertyLayoutSpec& layoutSpec = MakePropertyLayout(), float dragSpeed = 0.1f, float min = 0.0f, float max = 0.0f, const char* format = "%.2f" ) { - return DrawControlRow(label, columnWidth, [&]() { + return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) { + SetNextPropertyControlWidth(layout); return ImGui::DragFloat("##value", &value, dragSpeed, min, max, format); }); } +inline bool DrawLinearSlider( + const char* id, + float width, + float normalizedValue) { + const float clampedValue = std::clamp(normalizedValue, 0.0f, 1.0f); + const float frameHeight = ImGui::GetFrameHeight(); + ImGui::InvisibleButton(id, ImVec2(width, frameHeight)); + + const bool hovered = ImGui::IsItemHovered(); + const bool active = ImGui::IsItemActive(); + const ImVec2 min = ImGui::GetItemRectMin(); + const ImVec2 max = ImGui::GetItemRectMax(); + const float trackPadding = LinearSliderHorizontalPadding(); + const float trackMinX = min.x + trackPadding; + const float trackMaxX = max.x - trackPadding; + const float centerY = (min.y + max.y) * 0.5f; + const float knobX = trackMinX + (trackMaxX - trackMinX) * clampedValue; + const float trackHalfThickness = LinearSliderTrackThickness() * 0.5f; + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled( + ImVec2(trackMinX, centerY - trackHalfThickness), + ImVec2(trackMaxX, centerY + trackHalfThickness), + ImGui::GetColorU32(LinearSliderTrackColor()), + trackHalfThickness); + drawList->AddRectFilled( + ImVec2(trackMinX, centerY - trackHalfThickness), + ImVec2(knobX, centerY + trackHalfThickness), + ImGui::GetColorU32(LinearSliderFillColor()), + trackHalfThickness); + + const ImVec4 grabColor = active + ? LinearSliderGrabActiveColor() + : (hovered ? LinearSliderGrabHoveredColor() : LinearSliderGrabColor()); + drawList->AddCircleFilled( + ImVec2(knobX, centerY), + LinearSliderGrabRadius(), + ImGui::GetColorU32(grabColor), + 20); + + if (!active) { + return false; + } + + const float trackWidth = std::max(trackMaxX - trackMinX, 1.0f); + const float mouseRatio = std::clamp((ImGui::GetIO().MousePos.x - trackMinX) / trackWidth, 0.0f, 1.0f); + return std::fabs(mouseRatio - clampedValue) > 0.0001f; +} + inline bool DrawInt( const char* label, int& value, - float columnWidth = DefaultControlLabelWidth(), + const PropertyLayoutSpec& layoutSpec = MakePropertyLayout(), int step = 1, int min = 0, int max = 0 ) { - return DrawControlRow(label, columnWidth, [&]() { + return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) { + SetNextPropertyControlWidth(layout); return ImGui::DragInt("##value", &value, static_cast(step), min, max); }); } @@ -37,9 +167,9 @@ inline bool DrawInt( inline bool DrawBool( const char* label, bool& value, - float columnWidth = DefaultControlLabelWidth() + const PropertyLayoutSpec& layoutSpec = MakePropertyLayout() ) { - return DrawControlRow(label, columnWidth, [&]() { + return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics&) { return ImGui::Checkbox("##value", &value); }); } @@ -47,9 +177,9 @@ inline bool DrawBool( inline bool DrawColor3( const char* label, float color[3], - float columnWidth = DefaultControlLabelWidth() + const PropertyLayoutSpec& layoutSpec = MakePropertyLayout() ) { - return DrawControlRow(label, columnWidth, [&]() { + return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics&) { return ImGui::ColorEdit3("##value", color, ImGuiColorEditFlags_NoInputs); }); } @@ -57,9 +187,9 @@ inline bool DrawColor3( inline bool DrawColor4( const char* label, float color[4], - float columnWidth = DefaultControlLabelWidth() + const PropertyLayoutSpec& layoutSpec = MakePropertyLayout() ) { - return DrawControlRow(label, columnWidth, [&]() { + return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics&) { return ImGui::ColorEdit4("##value", color, ImGuiColorEditFlags_NoInputs); }); } @@ -69,11 +199,33 @@ inline bool DrawSliderFloat( float& value, float min, float max, - float columnWidth = DefaultControlLabelWidth(), + const PropertyLayoutSpec& layoutSpec = MakePropertyLayout(), const char* format = "%.2f" ) { - return DrawControlRow(label, columnWidth, [&]() { - return ImGui::SliderFloat("##value", &value, min, max, format); + return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) { + const float totalWidth = layout.controlWidth; + const float inputWidth = SliderValueFieldWidth(); + const float spacing = CompoundControlSpacing(); + const float sliderWidth = ImMax(totalWidth - inputWidth - spacing, 1.0f); + const float range = max - min; + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, 0.0f)); + float normalizedValue = range > 0.0f ? (value - min) / range : 0.0f; + bool changed = DrawLinearSlider("##slider", sliderWidth, normalizedValue); + if (ImGui::IsItemActive()) { + const float trackWidth = std::max(sliderWidth - LinearSliderHorizontalPadding() * 2.0f, 1.0f); + const float mouseRatio = std::clamp( + (ImGui::GetIO().MousePos.x - (ImGui::GetItemRectMin().x + LinearSliderHorizontalPadding())) / trackWidth, + 0.0f, + 1.0f); + value = min + range * mouseRatio; + } + ImGui::SameLine(0.0f, spacing); + ImGui::SetNextItemWidth(inputWidth); + changed = ImGui::InputFloat("##value", &value, 0.0f, 0.0f, format) || changed; + value = std::clamp(value, min, max); + ImGui::PopStyleVar(); + return changed; }); } @@ -82,10 +234,32 @@ inline bool DrawSliderInt( int& value, int min, int max, - float columnWidth = DefaultControlLabelWidth() + const PropertyLayoutSpec& layoutSpec = MakePropertyLayout() ) { - return DrawControlRow(label, columnWidth, [&]() { - return ImGui::SliderInt("##value", &value, min, max); + return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) { + const float totalWidth = layout.controlWidth; + const float inputWidth = SliderValueFieldWidth(); + const float spacing = CompoundControlSpacing(); + const float sliderWidth = ImMax(totalWidth - inputWidth - spacing, 1.0f); + const int range = max - min; + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, 0.0f)); + const float normalizedValue = range > 0 ? static_cast(value - min) / static_cast(range) : 0.0f; + bool changed = DrawLinearSlider("##slider", sliderWidth, normalizedValue); + if (ImGui::IsItemActive()) { + const float trackWidth = std::max(sliderWidth - LinearSliderHorizontalPadding() * 2.0f, 1.0f); + const float mouseRatio = std::clamp( + (ImGui::GetIO().MousePos.x - (ImGui::GetItemRectMin().x + LinearSliderHorizontalPadding())) / trackWidth, + 0.0f, + 1.0f); + value = min + static_cast(std::round(mouseRatio * static_cast(range))); + } + ImGui::SameLine(0.0f, spacing); + ImGui::SetNextItemWidth(inputWidth); + changed = ImGui::InputInt("##value", &value, 0, 0) || changed; + value = std::clamp(value, min, max); + ImGui::PopStyleVar(); + return changed; }); } @@ -94,14 +268,42 @@ inline int DrawCombo( int currentItem, const char* const items[], int itemCount, - float columnWidth = DefaultControlLabelWidth(), + const PropertyLayoutSpec& layoutSpec = MakePropertyLayout(), int heightInItems = -1 ) { int changedItem = currentItem; - DrawControlRow(label, columnWidth, [&]() { - ImGui::SetNextItemWidth(-1); - if (ImGui::Combo("##value", ¤tItem, items, itemCount, heightInItems)) { - changedItem = currentItem; + DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) { + const char* popupId = "##value_popup"; + const char* previewValue = + (currentItem >= 0 && currentItem < itemCount && items[currentItem] != nullptr) + ? items[currentItem] + : ""; + const float comboWidth = layout.controlWidth; + const float popupWidth = comboWidth; + DrawComboPreviewFrame("##value", previewValue, comboWidth, ImGui::IsPopupOpen(popupId)); + + const ImVec2 comboMin = ImGui::GetItemRectMin(); + const ImVec2 comboMax = ImGui::GetItemRectMax(); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + ImGui::OpenPopup(popupId); + } + + ImGui::SetNextWindowPos(ImVec2(comboMin.x, comboMax.y), ImGuiCond_Always); + ImGui::SetNextWindowSizeConstraints( + ImVec2(popupWidth, 0.0f), + ImVec2(popupWidth, CalcComboPopupMaxHeightFromItemCount(heightInItems))); + if (BeginStyledComboPopup(popupId, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings)) { + for (int itemIndex = 0; itemIndex < itemCount; ++itemIndex) { + const bool selected = itemIndex == currentItem; + if (ImGui::Selectable(items[itemIndex], selected)) { + changedItem = itemIndex; + ImGui::CloseCurrentPopup(); + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + EndStyledComboPopup(); } return false; }); diff --git a/editor/src/UI/StyleTokens.h b/editor/src/UI/StyleTokens.h index a3a652cb..0e1d60e3 100644 --- a/editor/src/UI/StyleTokens.h +++ b/editor/src/UI/StyleTokens.h @@ -94,22 +94,82 @@ inline ImVec2 DefaultPanelContentPadding() { return ImVec2(8.0f, 6.0f); } -inline float InspectorPropertyLabelWidth() { - return 104.0f; +inline ImVec2 HierarchyPanelContentPadding() { + return ImVec2(10.0f, 6.0f); } -inline ImVec2 ControlCellPadding() { - return ImVec2(0.0f, 2.0f); +inline float InspectorSectionContentIndent() { + return 12.0f; +} + +inline float InspectorPropertyLabelInset() { + return 12.0f; +} + +inline float InspectorPropertyControlColumnStart() { + return 236.0f; +} + +inline float InspectorPropertyLabelControlGap() { + return 20.0f; } inline ImVec2 ControlFramePadding() { - return ImVec2(6.0f, 3.0f); + return ImVec2(3.0f, 0.0f); } inline float ControlRowHeightOffset() { + return 1.0f; +} + +inline ImVec2 InspectorComponentControlSpacing() { + return ImVec2(6.0f, 3.0f); +} + +inline float InspectorPropertyControlTrailingInset() { + return 8.0f; +} + +inline float CompoundControlSpacing() { + return 4.0f; +} + +inline float SliderValueFieldWidth() { + return 64.0f; +} + +inline float LinearSliderTrackThickness() { return 2.0f; } +inline float LinearSliderGrabRadius() { + return 5.0f; +} + +inline float LinearSliderHorizontalPadding() { + return 5.0f; +} + +inline ImVec4 LinearSliderTrackColor() { + return ImVec4(0.30f, 0.30f, 0.30f, 1.0f); +} + +inline ImVec4 LinearSliderFillColor() { + return ImVec4(0.66f, 0.66f, 0.66f, 1.0f); +} + +inline ImVec4 LinearSliderGrabColor() { + return ImVec4(0.78f, 0.78f, 0.78f, 1.0f); +} + +inline ImVec4 LinearSliderGrabHoveredColor() { + return ImVec4(0.88f, 0.88f, 0.88f, 1.0f); +} + +inline ImVec4 LinearSliderGrabActiveColor() { + return ImVec4(0.95f, 0.95f, 0.95f, 1.0f); +} + inline ImVec2 InspectorPanelContentPadding() { return ImVec2(10.0f, 0.0f); } @@ -147,15 +207,15 @@ inline ImVec4 PanelSplitterIdleColor() { } inline ImVec4 PanelSplitterHoveredColor() { - return ImVec4(0.30f, 0.30f, 0.30f, 1.0f); + return PanelSplitterIdleColor(); } inline ImVec4 PanelSplitterActiveColor() { - return ImVec4(0.34f, 0.34f, 0.34f, 1.0f); + return PanelSplitterIdleColor(); } inline ImVec2 ProjectNavigationPanePadding() { - return ImVec2(8.0f, 6.0f); + return ImVec2(10.0f, 6.0f); } inline ImVec2 ProjectBrowserPanePadding() { @@ -163,21 +223,37 @@ inline ImVec2 ProjectBrowserPanePadding() { } inline ImVec2 NavigationTreeNodeFramePadding() { - return ImVec2(4.0f, 3.0f); + return ImVec2(4.0f, 1.0f); +} + +inline float CompactNavigationTreeItemSpacingY() { + return 0.0f; +} + +inline float CompactNavigationTreeIndentSpacing() { + return 14.0f; +} + +inline float NavigationTreeIconSize() { + return 17.0f; } inline float NavigationTreePrefixWidth() { - return 16.0f; -} - -inline float NavigationTreePrefixLabelGap() { - return 6.0f; + return 18.0f; } inline float NavigationTreePrefixStartOffset() { + return -5.0f; +} + +inline float NavigationTreePrefixLabelGap() { return 2.0f; } +inline float DisclosureArrowScale() { + return 0.14f; +} + inline ImVec4 NavigationTreePrefixColor(bool selected = false, bool hovered = false) { if (selected) { return ImVec4(0.86f, 0.86f, 0.86f, 1.0f); @@ -188,16 +264,54 @@ inline ImVec4 NavigationTreePrefixColor(bool selected = false, bool hovered = fa return ImVec4(0.62f, 0.62f, 0.62f, 1.0f); } +struct TreeViewStyle { + ImVec2 framePadding = NavigationTreeNodeFramePadding(); + float itemSpacingY = -1.0f; + float indentSpacing = -1.0f; + float prefixStartOffset = NavigationTreePrefixStartOffset(); + float prefixLabelGap = NavigationTreePrefixLabelGap(); +}; + +inline TreeViewStyle NavigationTreeStyle() { + TreeViewStyle style; + style.itemSpacingY = CompactNavigationTreeItemSpacingY(); + style.indentSpacing = CompactNavigationTreeIndentSpacing(); + style.framePadding = NavigationTreeNodeFramePadding(); + style.prefixStartOffset = NavigationTreePrefixStartOffset(); + style.prefixLabelGap = NavigationTreePrefixLabelGap(); + return style; +} + +inline TreeViewStyle HierarchyTreeStyle() { + return NavigationTreeStyle(); +} + +inline TreeViewStyle ProjectFolderTreeStyle() { + return NavigationTreeStyle(); +} + +inline ImVec4 ProjectPanelBackgroundColor() { + return DockTabSelectedColor(); +} + +inline ImVec4 ProjectPanelToolbarBackgroundColor() { + return ProjectPanelBackgroundColor(); +} + inline ImVec4 ProjectNavigationPaneBackgroundColor() { + return ProjectPanelBackgroundColor(); +} + +inline ImVec4 ProjectBrowserSurfaceColor() { return ImVec4(0.20f, 0.20f, 0.20f, 1.0f); } inline ImVec4 ProjectBrowserHeaderBackgroundColor() { - return ImVec4(0.18f, 0.18f, 0.18f, 1.0f); + return ProjectPanelBackgroundColor(); } inline ImVec4 ProjectBrowserPaneBackgroundColor() { - return ImVec4(0.22f, 0.22f, 0.22f, 1.0f); + return ProjectBrowserSurfaceColor(); } inline ImVec4 ToolbarBackgroundColor() { @@ -228,6 +342,28 @@ inline ImVec4 HintTextColor() { return ImVec4(0.53f, 0.53f, 0.53f, 1.0f); } +inline ImVec2 BreadcrumbSegmentPadding() { + return ImVec2(4.0f, 1.0f); +} + +inline float BreadcrumbSegmentSpacing() { + return 3.0f; +} + +inline ImVec4 BreadcrumbSegmentTextColor(bool current = false, bool hovered = false) { + if (hovered) { + return ImVec4(0.90f, 0.90f, 0.90f, 1.0f); + } + if (current) { + return ImVec4(0.84f, 0.84f, 0.84f, 1.0f); + } + return ImVec4(0.72f, 0.72f, 0.72f, 1.0f); +} + +inline ImVec4 BreadcrumbSeparatorColor() { + return HintTextColor(); +} + inline float EmptyStateLineOffset() { return 20.0f; } @@ -248,6 +384,122 @@ inline ImVec2 PopupWindowPadding() { return ImVec2(12.0f, 10.0f); } +inline ImVec2 ComboPopupWindowPadding() { + return ImVec2(6.0f, 1.0f); +} + +inline ImVec2 ComboPopupItemSpacing() { + return ImVec2(0.0f, 0.0f); +} + +inline float ComboPopupRounding() { + return 1.0f; +} + +inline float ComboPopupBorderSize() { + return 1.0f; +} + +inline ImVec4 ComboPopupBackgroundColor() { + return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); +} + +inline ImVec4 ComboPopupTextColor() { + return ImVec4(0.14f, 0.14f, 0.14f, 1.0f); +} + +inline ImVec4 ComboPopupTextDisabledColor() { + return ImVec4(0.55f, 0.55f, 0.55f, 1.0f); +} + +inline ImVec4 ComboPopupItemColor() { + return ImVec4(0.0f, 0.0f, 0.0f, 0.0f); +} + +inline ImVec4 ComboPopupItemHoveredColor() { + return ImVec4(0.0f, 0.0f, 0.0f, 0.045f); +} + +inline ImVec4 ComboPopupItemActiveColor() { + return ImVec4(0.0f, 0.0f, 0.0f, 0.08f); +} + +inline ImVec4 ComboPopupCheckMarkColor() { + return ImVec4(0.20f, 0.20f, 0.20f, 1.0f); +} + +inline float PopupWindowRounding() { + return 5.0f; +} + +inline float PopupWindowBorderSize() { + return 0.0f; +} + +inline float PopupFrameRounding() { + return 4.0f; +} + +inline float PopupFrameBorderSize() { + return 1.0f; +} + +inline ImVec4 PopupBackgroundColor() { + return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); +} + +inline ImVec4 PopupBorderColor() { + return ImVec4(0.0f, 0.0f, 0.0f, 0.10f); +} + +inline ImVec4 PopupTextColor() { + return ImVec4(0.14f, 0.14f, 0.14f, 1.0f); +} + +inline ImVec4 PopupTextDisabledColor() { + return ImVec4(0.55f, 0.55f, 0.55f, 1.0f); +} + +inline ImVec4 PopupItemColor() { + return ImVec4(0.0f, 0.0f, 0.0f, 0.0f); +} + +inline ImVec4 PopupItemHoveredColor() { + return ImVec4(0.0f, 0.0f, 0.0f, 0.06f); +} + +inline ImVec4 PopupItemActiveColor() { + return ImVec4(0.0f, 0.0f, 0.0f, 0.10f); +} + +inline ImVec4 PopupFrameColor() { + return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); +} + +inline ImVec4 PopupFrameHoveredColor() { + return ImVec4(0.965f, 0.965f, 0.965f, 1.0f); +} + +inline ImVec4 PopupFrameActiveColor() { + return ImVec4(0.94f, 0.94f, 0.94f, 1.0f); +} + +inline ImVec4 PopupButtonColor() { + return ImVec4(0.95f, 0.95f, 0.95f, 1.0f); +} + +inline ImVec4 PopupButtonHoveredColor() { + return ImVec4(0.90f, 0.90f, 0.90f, 1.0f); +} + +inline ImVec4 PopupButtonActiveColor() { + return ImVec4(0.86f, 0.86f, 0.86f, 1.0f); +} + +inline ImVec4 PopupCheckMarkColor() { + return ImVec4(0.20f, 0.20f, 0.20f, 1.0f); +} + inline ImVec2 AssetTileSize() { return ImVec2(104.0f, 82.0f); } @@ -304,6 +556,18 @@ inline ImVec2 AssetTileIconSize() { return ImVec2(32.0f, 24.0f); } +inline ImVec2 FolderAssetTileIconSize() { + return ImVec2(72.0f, 72.0f); +} + +inline ImVec2 FolderAssetTileIconOffset() { + return ImVec2(0.0f, 2.0f); +} + +inline float AssetTileIconTextGap() { + return 4.0f; +} + inline ImVec2 AssetTileTextPadding() { return ImVec2(6.0f, 10.0f); } @@ -312,12 +576,16 @@ inline ImVec4 AssetTileTextColor(bool selected = false) { return selected ? ImVec4(0.93f, 0.93f, 0.93f, 1.0f) : ImVec4(0.76f, 0.76f, 0.76f, 1.0f); } -inline ImVec4 AssetFolderIconFillColor() { - return ImVec4(0.50f, 0.50f, 0.50f, 1.0f); +inline ImVec4 BuiltInFolderIconTabColor() { + return ImVec4(0.77f, 0.77f, 0.77f, 1.0f); } -inline ImVec4 AssetFolderIconLineColor() { - return ImVec4(0.80f, 0.80f, 0.80f, 0.90f); +inline ImVec4 BuiltInFolderIconTopColor() { + return ImVec4(0.82f, 0.82f, 0.82f, 1.0f); +} + +inline ImVec4 BuiltInFolderIconBodyColor() { + return ImVec4(0.72f, 0.72f, 0.72f, 1.0f); } inline ImVec4 AssetFileIconFillColor() { diff --git a/editor/src/UI/TreeView.h b/editor/src/UI/TreeView.h index 118302cf..0aaa957a 100644 --- a/editor/src/UI/TreeView.h +++ b/editor/src/UI/TreeView.h @@ -1,12 +1,13 @@ #pragma once -#include "StyleTokens.h" +#include "Core.h" #include #include #include #include #include +#include namespace XCEngine { namespace Editor { @@ -90,91 +91,135 @@ struct TreeNodeCallbacks { struct TreeNodeDefinition { TreeNodeOptions options; std::string_view persistenceKey; + TreeViewStyle style; TreeNodePrefixSlot prefix; TreeNodeCallbacks callbacks; }; +inline std::vector& TreeIndentStack() { + static std::vector stack; + return stack; +} + +inline void ResetTreeLayout() { + TreeIndentStack().clear(); +} + inline TreeNodeResult DrawTreeNode( TreeViewState* state, const void* id, const char* label, const TreeNodeDefinition& definition = {}) { + const char* nodeLabel = label ? label : ""; TreeNodeOptions options = definition.options; - if (state && !definition.persistenceKey.empty()) { - ImGui::SetNextItemOpen(state->ResolveExpanded(definition.persistenceKey, options.defaultOpen), ImGuiCond_Always); - } - ImGuiTreeNodeFlags flags = 0; - if (options.openOnArrow) { - flags |= ImGuiTreeNodeFlags_OpenOnArrow; - } - if (options.openOnDoubleClick) { - flags |= ImGuiTreeNodeFlags_OpenOnDoubleClick; - } - if (options.spanAvailWidth) { - flags |= ImGuiTreeNodeFlags_SpanAvailWidth; - } - if (options.framePadding) { - flags |= ImGuiTreeNodeFlags_FramePadding; - } - if (options.leaf) { - flags |= ImGuiTreeNodeFlags_Leaf; - } - if (options.selected) { - flags |= ImGuiTreeNodeFlags_Selected; - } - if (options.defaultOpen) { - flags |= ImGuiTreeNodeFlags_DefaultOpen; - } + const TreeViewStyle& visualStyle = definition.style; + const ImVec2 nodeFramePadding = options.framePadding ? visualStyle.framePadding : ImVec2(0.0f, 0.0f); + const ImVec2 currentItemSpacing = ImGui::GetStyle().ItemSpacing; + const float itemSpacingY = visualStyle.itemSpacingY >= 0.0f ? visualStyle.itemSpacingY : currentItemSpacing.y; + const float indentSpacing = visualStyle.indentSpacing >= 0.0f ? visualStyle.indentSpacing : ImGui::GetStyle().IndentSpacing; + const float depth = static_cast(TreeIndentStack().size()); + const float treeRootX = ImGui::GetCursorStartPos().x; + const float arrowSlotWidth = ImGui::GetTreeNodeToLabelSpacing(); + const float prefixWidth = definition.prefix.IsVisible() ? definition.prefix.width : 0.0f; + const float prefixGap = definition.prefix.IsVisible() ? visualStyle.prefixLabelGap : 0.0f; + const ImVec2 labelSize = ImGui::CalcTextSize(nodeLabel, nullptr, false); + const float rowHeight = ImMax(labelSize.y, ImGui::GetFontSize()) + nodeFramePadding.y * 2.0f; + ImGui::SetCursorPosX(treeRootX + depth * indentSpacing); + const float availableWidth = options.spanAvailWidth + ? ImMax(ImGui::GetContentRegionAvail().x, 1.0f) + : arrowSlotWidth + visualStyle.prefixStartOffset + prefixWidth + prefixGap + labelSize.x + nodeFramePadding.x; - std::string displayLabel = label ? label : ""; - if (definition.prefix.IsVisible()) { - const float reserveWidth = - NavigationTreePrefixStartOffset() + - definition.prefix.width + - NavigationTreePrefixLabelGap(); - const float spaceWidth = ImGui::CalcTextSize(" ").x; - int spaceCount = static_cast(reserveWidth / (spaceWidth > 0.0f ? spaceWidth : 1.0f)) + 2; - if (spaceCount < 1) { - spaceCount = 1; - } - - displayLabel.insert(0, static_cast(spaceCount), ' '); - } - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, NavigationTreeNodeFramePadding()); - const bool open = ImGui::TreeNodeEx(id, flags, "%s", displayLabel.c_str()); + ImGui::PushID(id); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(currentItemSpacing.x, itemSpacingY)); + ImGui::InvisibleButton("##TreeNode", ImVec2(availableWidth, rowHeight)); ImGui::PopStyleVar(); - TreeNodeResult result{ - open, - ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen(), - ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0), - ImGui::IsItemClicked(ImGuiMouseButton_Right), - ImGui::IsItemHovered(), - ImGui::IsItemToggledOpen() - }; - - if (state && !definition.persistenceKey.empty()) { - state->SetExpanded(definition.persistenceKey, result.open); - } - const ImVec2 itemMin = ImGui::GetItemRectMin(); const ImVec2 itemMax = ImGui::GetItemRectMax(); - const float labelStartX = itemMin.x + ImGui::GetTreeNodeToLabelSpacing(); + const ImRect rowRect(itemMin, itemMax); + const ImRect arrowRect( + ImVec2(itemMin.x + nodeFramePadding.x, itemMin.y), + ImVec2(itemMin.x + arrowSlotWidth, itemMax.y)); + + bool open = !options.leaf && options.defaultOpen; + if (state && !definition.persistenceKey.empty()) { + open = !options.leaf && state->ResolveExpanded(definition.persistenceKey, options.defaultOpen); + } + + const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + const bool leftClicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); + const bool rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right); + const bool doubleClicked = hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left); + const bool arrowHovered = !options.leaf && arrowRect.Contains(ImGui::GetIO().MousePos); + + bool toggledOpen = false; + if (!options.leaf) { + if (options.openOnArrow && leftClicked && arrowHovered) { + toggledOpen = true; + } else if (!options.openOnArrow && leftClicked) { + toggledOpen = true; + } else if (options.openOnDoubleClick && doubleClicked && !arrowHovered) { + toggledOpen = true; + } + } + + if (toggledOpen) { + open = !open; + } + + if (state && !definition.persistenceKey.empty()) { + state->SetExpanded(definition.persistenceKey, open); + } + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + if (options.selected || hovered) { + const ImU32 rowColor = ImGui::GetColorU32( + (options.selected && hovered) ? ImGuiCol_HeaderActive : + options.selected ? ImGuiCol_Header : + ImGuiCol_HeaderHovered); + drawList->AddRectFilled(rowRect.Min, rowRect.Max, rowColor, 0.0f); + } + + if (!options.leaf) { + DrawDisclosureArrow(drawList, arrowRect.Min, arrowRect.Max, open, ImGui::GetColorU32(ImGuiCol_Text)); + } + + const float baseTextX = itemMin.x + arrowSlotWidth; + float labelX = baseTextX; if (definition.prefix.IsVisible()) { - const float prefixMinX = labelStartX + NavigationTreePrefixStartOffset(); + const float prefixMinX = baseTextX + visualStyle.prefixStartOffset; const float prefixMaxX = prefixMinX + definition.prefix.width; definition.prefix.draw(TreeNodePrefixContext{ - ImGui::GetWindowDrawList(), + drawList, ImVec2(prefixMinX, itemMin.y), ImVec2(prefixMaxX, itemMax.y), options.selected, - result.hovered + hovered }); + labelX = prefixMaxX + visualStyle.prefixLabelGap; } + if (nodeLabel[0] != '\0') { + const float textY = itemMin.y + ((itemMax.y - itemMin.y) - labelSize.y) * 0.5f; + drawList->PushClipRect(ImVec2(labelX, itemMin.y), itemMax, true); + drawList->AddText( + ImVec2(labelX, textY), + ImGui::GetColorU32(ImGuiCol_Text), + nodeLabel); + drawList->PopClipRect(); + } + + TreeNodeResult result{ + open, + leftClicked && !toggledOpen, + doubleClicked, + rightClicked, + hovered, + toggledOpen + }; + if (definition.callbacks.onInteraction) { definition.callbacks.onInteraction(result); } @@ -182,6 +227,12 @@ inline TreeNodeResult DrawTreeNode( definition.callbacks.onRenderExtras(); } + if (open) { + TreeIndentStack().push_back(indentSpacing); + } + + ImGui::PopID(); + return result; } @@ -192,7 +243,11 @@ inline TreeNodeResult DrawTreeNode(const void* id, const char* label, const Tree } inline void EndTreeNode() { - ImGui::TreePop(); + auto& stack = TreeIndentStack(); + if (stack.empty()) { + return; + } + stack.pop_back(); } } // namespace UI diff --git a/editor/src/UI/UI.h b/editor/src/UI/UI.h index 696af9dc..df15f5ca 100644 --- a/editor/src/UI/UI.h +++ b/editor/src/UI/UI.h @@ -2,6 +2,7 @@ #include "AboutEditorDialog.h" #include "BaseTheme.h" +#include "BuiltInIcons.h" #include "ConsoleFilterState.h" #include "ConsoleLogFormatter.h" #include "Core.h" @@ -10,6 +11,7 @@ #include "DividerChrome.h" #include "PanelChrome.h" #include "PopupState.h" +#include "PropertyLayout.h" #include "PropertyGrid.h" #include "SplitterChrome.h" #include "ScalarControls.h" diff --git a/editor/src/UI/VectorControls.h b/editor/src/UI/VectorControls.h index 19643db0..7e9917e9 100644 --- a/editor/src/UI/VectorControls.h +++ b/editor/src/UI/VectorControls.h @@ -1,6 +1,7 @@ #pragma once #include "Core.h" +#include "PropertyLayout.h" #include #include #include @@ -16,6 +17,7 @@ struct AxisFloatControlSpec { }; inline bool DrawAxisFloatControls( + const PropertyLayoutMetrics& layout, const AxisFloatControlSpec* axes, int axisCount, float dragSpeed, @@ -30,7 +32,7 @@ inline bool DrawAxisFloatControls( ImGuiStyleVar_ItemSpacing, useResetButtons ? VectorAxisControlSpacing() : VectorAxisInputSpacing()); - const float availableWidth = ImGui::GetContentRegionAvail().x; + const float availableWidth = layout.controlWidth; const float spacing = ImGui::GetStyle().ItemSpacing.x; if (useResetButtons) { @@ -101,7 +103,7 @@ inline bool DrawAxisFloatControls( inline bool DrawVec3Input( const char* label, ::XCEngine::Math::Vector3& values, - float columnWidth = DefaultControlLabelWidth(), + const PropertyLayoutSpec& layoutSpec = MakePropertyLayout(), float dragSpeed = 0.1f, bool* isActive = nullptr ) { @@ -111,8 +113,8 @@ inline bool DrawVec3Input( { "Z", &values.z } }; - return DrawControlRow(label, columnWidth, [&]() { - return DrawAxisFloatControls(axes, 3, dragSpeed, false, 0.0f, isActive); + return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) { + return DrawAxisFloatControls(layout, axes, 3, dragSpeed, false, 0.0f, isActive); }); } @@ -120,7 +122,7 @@ inline bool DrawVec3( const char* label, ::XCEngine::Math::Vector3& values, float resetValue = 0.0f, - float columnWidth = DefaultControlLabelWidth(), + const PropertyLayoutSpec& layoutSpec = MakePropertyLayout(), float dragSpeed = 0.1f, bool* isActive = nullptr ) { @@ -130,8 +132,8 @@ inline bool DrawVec3( { "Z", &values.z } }; - return DrawControlRow(label, columnWidth, [&]() { - return DrawAxisFloatControls(axes, 3, dragSpeed, true, resetValue, isActive); + return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) { + return DrawAxisFloatControls(layout, axes, 3, dragSpeed, true, resetValue, isActive); }); } @@ -139,7 +141,7 @@ inline bool DrawVec2( const char* label, ::XCEngine::Math::Vector2& values, float resetValue = 0.0f, - float columnWidth = DefaultControlLabelWidth(), + const PropertyLayoutSpec& layoutSpec = MakePropertyLayout(), float dragSpeed = 0.1f ) { const AxisFloatControlSpec axes[] = { @@ -147,8 +149,8 @@ inline bool DrawVec2( { "Y", &values.y } }; - return DrawControlRow(label, columnWidth, [&]() { - return DrawAxisFloatControls(axes, 2, dragSpeed, true, resetValue); + return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) { + return DrawAxisFloatControls(layout, axes, 2, dragSpeed, true, resetValue); }); } diff --git a/editor/src/UI/Widgets.h b/editor/src/UI/Widgets.h index d6df4d49..7e25126f 100644 --- a/editor/src/UI/Widgets.h +++ b/editor/src/UI/Widgets.h @@ -12,6 +12,7 @@ namespace UI { struct ComponentSectionResult { bool open = false; + float contentIndent = 0.0f; }; struct AssetTileResult { @@ -23,9 +24,12 @@ struct AssetTileResult { ImVec2 max = ImVec2(0.0f, 0.0f); }; -enum class AssetIconKind { - Folder, - File +struct AssetTileOptions { + ImVec2 size = AssetTileSize(); + ImVec2 iconOffset = AssetTileIconOffset(); + ImVec2 iconSize = AssetTileIconSize(); + bool drawIdleFrame = true; + bool drawSelectionBorder = true; }; enum class DialogActionResult { @@ -61,11 +65,16 @@ struct MenuCommand { template inline bool DrawMenuScope(const char* label, DrawContentFn&& drawContent) { + PushPopupWindowChrome(); if (!ImGui::BeginMenu(label)) { + PopPopupWindowChrome(); return false; } + PopPopupWindowChrome(); + PushPopupContentChrome(); drawContent(); + PopPopupContentChrome(); ImGui::EndMenu(); return true; } @@ -142,39 +151,105 @@ inline void DrawEmptyState( } } +inline float BreadcrumbItemHeight() { + return ImGui::GetTextLineHeight() + BreadcrumbSegmentPadding().y * 2.0f; +} + +inline void DrawBreadcrumbTextItem( + const char* label, + const ImVec2& size, + const ImVec4& color, + bool clickable, + bool* pressed = nullptr, + bool* hovered = nullptr) { + bool localPressed = false; + if (clickable) { + localPressed = ImGui::InvisibleButton("##BreadcrumbItem", size); + } else { + ImGui::Dummy(size); + } + const bool localHovered = clickable && ImGui::IsItemHovered(); + + const ImVec2 itemMin = ImGui::GetItemRectMin(); + const ImVec2 itemMax = ImGui::GetItemRectMax(); + const ImVec2 textSize = ImGui::CalcTextSize(label); + const float textX = itemMin.x + (size.x - textSize.x) * 0.5f; + const float textY = itemMin.y + (itemMax.y - itemMin.y - textSize.y) * 0.5f; + + ImGui::GetWindowDrawList()->AddText( + ImVec2(textX, textY), + ImGui::GetColorU32(color), + label); + + if (pressed) { + *pressed = localPressed; + } + if (hovered) { + *hovered = localHovered; + } +} + +inline bool DrawBreadcrumbSegment(const char* label, bool clickable, bool current = false) { + if (!label || label[0] == '\0') { + return false; + } + + const ImVec2 padding = BreadcrumbSegmentPadding(); + const ImVec2 textSize = ImGui::CalcTextSize(label); + const ImVec2 size(textSize.x + padding.x * 2.0f, BreadcrumbItemHeight()); + bool pressed = false; + bool hovered = false; + DrawBreadcrumbTextItem(label, size, BreadcrumbSegmentTextColor(current, hovered), clickable, &pressed, &hovered); + + if (hovered) { + const ImVec2 itemMin = ImGui::GetItemRectMin(); + const ImVec2 itemMax = ImGui::GetItemRectMax(); + const ImVec2 textOnlySize = ImGui::CalcTextSize(label); + const float textX = itemMin.x + (size.x - textOnlySize.x) * 0.5f; + const float textY = itemMin.y + (itemMax.y - itemMin.y - textOnlySize.y) * 0.5f; + ImGui::GetWindowDrawList()->AddText( + ImVec2(textX, textY), + ImGui::GetColorU32(BreadcrumbSegmentTextColor(current, true)), + label); + } + + return pressed; +} + +inline void DrawBreadcrumbSeparator(const char* label = ">") { + const ImVec2 textSize = ImGui::CalcTextSize(label); + DrawBreadcrumbTextItem(label, ImVec2(textSize.x, BreadcrumbItemHeight()), BreadcrumbSeparatorColor(), false); +} + template inline void DrawToolbarBreadcrumbs( const char* rootLabel, size_t segmentCount, GetNameFn&& getName, NavigateFn&& navigateToSegment) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); - if (segmentCount == 0) { - ImGui::TextUnformatted(rootLabel); - ImGui::PopStyleColor(2); + DrawBreadcrumbSegment(rootLabel, false, true); return; } for (size_t i = 0; i < segmentCount; ++i) { if (i > 0) { - ImGui::SameLine(); - ImGui::TextDisabled("/"); - ImGui::SameLine(); + ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing()); + DrawBreadcrumbSeparator(); + ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing()); } - const std::string label = getName(i); - if (i + 1 < segmentCount) { - if (ImGui::Button(label.c_str())) { - navigateToSegment(i); - } - } else { - ImGui::Text("%s", label.c_str()); + const std::string label = (i == 0 && rootLabel && rootLabel[0] != '\0') + ? std::string(rootLabel) + : getName(i); + const bool current = (i + 1 == segmentCount); + + ImGui::PushID(static_cast(i)); + if (DrawBreadcrumbSegment(label.c_str(), !current, current)) { + navigateToSegment(i); } + ImGui::PopID(); } - - ImGui::PopStyleColor(2); } template @@ -182,8 +257,18 @@ inline AssetTileResult DrawAssetTile( const char* label, bool selected, bool dimmed, - DrawIconFn&& drawIcon) { - const ImVec2 tileSize = AssetTileSize(); + DrawIconFn&& drawIcon, + const AssetTileOptions& options = AssetTileOptions()) { + const ImVec2 textSize = ImGui::CalcTextSize(label); + ImVec2 tileSize = options.size; + tileSize.x = std::max(tileSize.x, options.iconSize.x + AssetTileTextPadding().x * 2.0f); + tileSize.y = std::max( + tileSize.y, + options.iconOffset.y + + options.iconSize.y + + AssetTileIconTextGap() + + textSize.y + + AssetTileTextPadding().y); ImGui::InvisibleButton("##AssetBtn", tileSize); const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); @@ -195,58 +280,38 @@ 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 (options.drawIdleFrame) { + 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()); } - if (selected) { + if (selected && options.drawSelectionBorder) { drawList->AddRect(min, max, ImGui::GetColorU32(AssetTileSelectedBorderColor()), AssetTileRounding()); } if (dimmed) { drawList->AddRectFilled(min, max, ImGui::GetColorU32(AssetTileDraggedOverlayColor()), 0.0f); } - const ImVec2 iconOffset = AssetTileIconOffset(); - const ImVec2 iconSize = AssetTileIconSize(); - const ImVec2 iconMin(min.x + iconOffset.x, min.y + iconOffset.y); - const ImVec2 iconMax(iconMin.x + iconSize.x, iconMin.y + iconSize.y); + const ImVec2 iconMin( + min.x + (tileSize.x - options.iconSize.x) * 0.5f, + min.y + options.iconOffset.y); + const ImVec2 iconMax(iconMin.x + options.iconSize.x, iconMin.y + options.iconSize.y); drawIcon(drawList, iconMin, iconMax); - const ImVec2 textSize = ImGui::CalcTextSize(label); + const ImVec2 textPadding = AssetTileTextPadding(); + const float textAreaWidth = tileSize.x - textPadding.x * 2.0f; + const float centeredTextX = min.x + textPadding.x + std::max(0.0f, (textAreaWidth - textSize.x) * 0.5f); const float textY = max.y - textSize.y - AssetTileTextPadding().y; - ImGui::PushClipRect(ImVec2(min.x + AssetTileTextPadding().x, min.y), ImVec2(max.x - AssetTileTextPadding().x, max.y), true); - drawList->AddText(ImVec2(min.x + AssetTileTextPadding().x, textY), ImGui::GetColorU32(AssetTileTextColor(selected)), label); + ImGui::PushClipRect(ImVec2(min.x + textPadding.x, min.y), ImVec2(max.x - textPadding.x, max.y), true); + drawList->AddText(ImVec2(centeredTextX, textY), ImGui::GetColorU32(AssetTileTextColor(selected)), label); ImGui::PopClipRect(); return AssetTileResult{ clicked, contextRequested, openRequested, hovered, min, max }; } -inline void DrawAssetIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, AssetIconKind kind) { - if (kind == AssetIconKind::Folder) { - const ImU32 fillColor = ImGui::GetColorU32(AssetFolderIconFillColor()); - const ImU32 lineColor = ImGui::GetColorU32(AssetFolderIconLineColor()); - const float width = max.x - min.x; - const float height = max.y - min.y; - const ImVec2 tabMax(min.x + width * 0.45f, min.y + height * 0.35f); - drawList->AddRectFilled(ImVec2(min.x, min.y + height * 0.14f), tabMax, fillColor, 2.0f); - drawList->AddRectFilled(ImVec2(min.x, min.y + height * 0.28f), max, fillColor, 2.0f); - drawList->AddRect(ImVec2(min.x, min.y + height * 0.14f), tabMax, lineColor, 2.0f); - drawList->AddRect(ImVec2(min.x, min.y + height * 0.28f), max, lineColor, 2.0f); - return; - } - - const ImU32 fillColor = ImGui::GetColorU32(AssetFileIconFillColor()); - const ImU32 lineColor = ImGui::GetColorU32(AssetFileIconLineColor()); - const ImVec2 foldA(max.x - 8.0f, min.y); - const ImVec2 foldB(max.x, min.y + 8.0f); - drawList->AddRectFilled(min, max, fillColor, 2.0f); - drawList->AddRect(min, max, lineColor, 2.0f); - drawList->AddTriangleFilled(foldA, ImVec2(max.x, min.y), foldB, ImGui::GetColorU32(AssetFileFoldColor())); - drawList->AddLine(foldA, foldB, lineColor); -} - template inline ComponentSectionResult BeginComponentSection( const void* id, @@ -254,27 +319,68 @@ inline ComponentSectionResult BeginComponentSection( DrawMenuFn&& drawMenu, bool defaultOpen = true) { const ImGuiStyle& style = ImGui::GetStyle(); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, InspectorSectionFramePadding()); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x, InspectorSectionItemSpacing().y)); + const ImVec2 framePadding = InspectorSectionFramePadding(); + const float availableWidth = ImMax(ImGui::GetContentRegionAvail().x, 1.0f); + const float arrowSlotWidth = ImGui::GetTreeNodeToLabelSpacing(); + const ImVec2 labelSize = ImGui::CalcTextSize(label ? label : "", nullptr, false); + const float rowHeight = ImMax(labelSize.y, ImGui::GetFontSize()) + framePadding.y * 2.0f; - ImGuiTreeNodeFlags flags = - ImGuiTreeNodeFlags_Framed | - ImGuiTreeNodeFlags_SpanAvailWidth | - ImGuiTreeNodeFlags_FramePadding | - ImGuiTreeNodeFlags_AllowOverlap; - if (defaultOpen) { - flags |= ImGuiTreeNodeFlags_DefaultOpen; + ImGui::PushID(id); + const ImGuiID openStateId = ImGui::GetID("##ComponentSectionOpen"); + ImGuiStorage* storage = ImGui::GetStateStorage(); + bool open = storage->GetBool(openStateId, defaultOpen); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x, InspectorSectionItemSpacing().y)); + ImGui::InvisibleButton("##ComponentSectionHeader", ImVec2(availableWidth, rowHeight)); + ImGui::PopStyleVar(); + + const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + const bool held = ImGui::IsItemActive(); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + open = !open; + storage->SetBool(openStateId, open); } - const bool open = ImGui::TreeNodeEx(id, flags, "%s", label); - ImGui::PopStyleVar(2); + const ImVec2 itemMin = ImGui::GetItemRectMin(); + const ImVec2 itemMax = ImGui::GetItemRectMax(); + const ImRect frameRect(itemMin, itemMax); + const ImRect arrowRect( + ImVec2(itemMin.x + framePadding.x, itemMin.y), + ImVec2(itemMin.x + arrowSlotWidth, itemMax.y)); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const ImU32 bgColor = ImGui::GetColorU32( + (held && hovered) ? ImGuiCol_HeaderActive : + hovered ? ImGuiCol_HeaderHovered : + ImGuiCol_Header); + drawList->AddRectFilled(frameRect.Min, frameRect.Max, bgColor, style.FrameRounding); + if (style.FrameBorderSize > 0.0f) { + drawList->AddRect( + frameRect.Min, + frameRect.Max, + ImGui::GetColorU32(ImGuiCol_Border), + style.FrameRounding, + 0, + style.FrameBorderSize); + } + + DrawDisclosureArrow(drawList, arrowRect.Min, arrowRect.Max, open, ImGui::GetColorU32(ImGuiCol_Text)); + + if (label && label[0] != '\0') { + const float textX = itemMin.x + arrowSlotWidth; + const float textY = itemMin.y + ((itemMax.y - itemMin.y) - labelSize.y) * 0.5f; + drawList->PushClipRect(ImVec2(textX, itemMin.y), itemMax, true); + drawList->AddText(ImVec2(textX, textY), ImGui::GetColorU32(ImGuiCol_Text), label); + drawList->PopClipRect(); + } if (BeginPopupContextItem("##ComponentSettings")) { drawMenu(); EndPopup(); } - return ComponentSectionResult{ open }; + ImGui::PopID(); + return ComponentSectionResult{ open, InspectorSectionContentIndent() }; } inline ComponentSectionResult BeginComponentSection( @@ -284,8 +390,10 @@ inline ComponentSectionResult BeginComponentSection( return BeginComponentSection(id, label, []() {}, defaultOpen); } -inline void EndComponentSection() { - ImGui::TreePop(); +inline void EndComponentSection(const ComponentSectionResult& section) { + if (section.open && section.contentIndent > 0.0f) { + ImGui::Unindent(section.contentIndent); + } } inline bool InspectorActionButton(const char* label, ImVec2 size = ImVec2(-1.0f, 0.0f)) { diff --git a/editor/src/panels/ConsolePanel.cpp b/editor/src/panels/ConsolePanel.cpp index b08e8c69..b23da1dc 100644 --- a/editor/src/panels/ConsolePanel.cpp +++ b/editor/src/panels/ConsolePanel.cpp @@ -1,7 +1,5 @@ -#include "Actions/ConsoleActionRouter.h" #include "Actions/ActionRouting.h" #include "ConsolePanel.h" -#include "Core/EditorConsoleSink.h" #include "UI/UI.h" #include @@ -18,22 +16,6 @@ void ConsolePanel::Render() { } Actions::ObserveInactiveActionRoute(*m_context); - - auto* sink = Debug::EditorConsoleSink::GetInstance(); - - { - UI::PanelToolbarScope toolbar("ConsoleToolbar", UI::StandardPanelToolbarHeight()); - if (toolbar.IsOpen()) { - Actions::DrawConsoleToolbarActions(*sink, m_filterState); - } - } - - UI::PanelContentScope content("LogScroll", UI::DefaultPanelContentPadding(), ImGuiWindowFlags_HorizontalScrollbar); - if (!content.IsOpen()) { - return; - } - - Actions::DrawConsoleLogRows(*sink, m_filterState); } } diff --git a/editor/src/panels/ConsolePanel.h b/editor/src/panels/ConsolePanel.h index 852284aa..1a3682c8 100644 --- a/editor/src/panels/ConsolePanel.h +++ b/editor/src/panels/ConsolePanel.h @@ -1,7 +1,6 @@ #pragma once #include "Panel.h" -#include "UI/ConsoleFilterState.h" namespace XCEngine { namespace Editor { @@ -10,9 +9,6 @@ class ConsolePanel : public Panel { public: ConsolePanel(); void Render() override; - -private: - UI::ConsoleFilterState m_filterState; }; } diff --git a/editor/src/panels/HierarchyPanel.cpp b/editor/src/panels/HierarchyPanel.cpp index 822122db..6278ecb3 100644 --- a/editor/src/panels/HierarchyPanel.cpp +++ b/editor/src/panels/HierarchyPanel.cpp @@ -17,14 +17,16 @@ void DrawHierarchyTreePrefix(const XCEngine::Editor::UI::TreeNodePrefixContext& return; } - const ImVec4 color = XCEngine::Editor::UI::NavigationTreePrefixColor(context.selected, context.hovered); - const float size = 9.0f; - const float centerX = context.min.x + (context.max.x - context.min.x) * 0.5f; - const float centerY = context.min.y + (context.max.y - context.min.y) * 0.5f; - const ImVec2 min(centerX - size * 0.5f, centerY - size * 0.5f); - const ImVec2 max(centerX + size * 0.5f, centerY + size * 0.5f); - context.drawList->AddRect(min, max, ImGui::GetColorU32(color), 1.5f); - context.drawList->AddLine(ImVec2(min.x, centerY), ImVec2(max.x, centerY), ImGui::GetColorU32(color), 1.0f); + const float width = context.max.x - context.min.x; + const float height = context.max.y - context.min.y; + const float iconExtent = XCEngine::Editor::UI::NavigationTreeIconSize(); + const float minX = context.min.x + (width - iconExtent) * 0.5f; + const float minY = context.min.y + (height - iconExtent) * 0.5f; + XCEngine::Editor::UI::DrawAssetIcon( + context.drawList, + ImVec2(minX, minY), + ImVec2(minX + iconExtent, minY + iconExtent), + XCEngine::Editor::UI::AssetIconKind::GameObject); } } // namespace @@ -95,7 +97,7 @@ void HierarchyPanel::Render() { Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Hierarchy); - UI::PanelContentScope content("EntityList"); + UI::PanelContentScope content("EntityList", UI::HierarchyPanelContentPadding()); if (!content.IsOpen()) { ImGui::PopStyleColor(2); return; @@ -103,6 +105,7 @@ void HierarchyPanel::Render() { auto& sceneManager = m_context->GetSceneManager(); auto rootEntities = sceneManager.GetRootEntities(); + UI::ResetTreeLayout(); for (auto* gameObject : rootEntities) { RenderEntity(gameObject); @@ -147,6 +150,7 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject nodeDefinition.options.leaf = gameObject->GetChildCount() == 0; const std::string persistenceKey = std::to_string(gameObject->GetUUID()); nodeDefinition.persistenceKey = persistenceKey; + nodeDefinition.style = UI::HierarchyTreeStyle(); nodeDefinition.prefix.width = UI::NavigationTreePrefixWidth(); nodeDefinition.prefix.draw = DrawHierarchyTreePrefix; nodeDefinition.callbacks.onInteraction = [this, gameObject](const UI::TreeNodeResult& node) { diff --git a/editor/src/panels/InspectorPanel.cpp b/editor/src/panels/InspectorPanel.cpp index bd38b34c..32303719 100644 --- a/editor/src/panels/InspectorPanel.cpp +++ b/editor/src/panels/InspectorPanel.cpp @@ -48,25 +48,33 @@ void InspectorPanel::OnSelectionChanged(const SelectionChangedEvent& event) { } void InspectorPanel::Render() { - UI::PanelWindowScope panel(m_name.c_str()); - if (!panel.IsOpen()) { - return; - } - - Actions::ObserveInactiveActionRoute(*m_context); - - if (m_selectedEntityId) { - auto& sceneManager = m_context->GetSceneManager(); - auto* gameObject = sceneManager.GetEntity(m_selectedEntityId); - if (gameObject) { - RenderGameObject(gameObject); - } else { - RenderEmptyState("Object not found"); + ImGui::PushStyleColor(ImGuiCol_WindowBg, UI::HierarchyInspectorPanelBackgroundColor()); + ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::HierarchyInspectorPanelBackgroundColor()); + { + UI::PanelWindowScope panel(m_name.c_str()); + if (!panel.IsOpen()) { + ImGui::PopStyleColor(2); + return; + } + + Actions::ObserveInactiveActionRoute(*m_context); + + if (m_selectedEntityId) { + auto& sceneManager = m_context->GetSceneManager(); + auto* gameObject = sceneManager.GetEntity(m_selectedEntityId); + if (gameObject) { + RenderGameObject(gameObject); + } else { + RenderEmptyState("Object not found"); + } + } else { + UI::PanelContentScope content( + "InspectorEmpty", + UI::InspectorPanelContentPadding(), + ImGuiChildFlags_None); } - } else { - RenderEmptyState("No Selection", "Select an object in Hierarchy"); } - + ImGui::PopStyleColor(2); } void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameObject) { @@ -74,6 +82,7 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb UI::PanelContentScope content( "InspectorContent", UI::InspectorPanelContentPadding(), + ImGuiChildFlags_None, ImGuiWindowFlags_None, true, ImVec2(style.ItemSpacing.x, 0.0f)); @@ -93,7 +102,10 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb } void InspectorPanel::RenderEmptyState(const char* title, const char* subtitle) { - UI::PanelContentScope content("InspectorEmptyState", UI::InspectorPanelContentPadding()); + UI::PanelContentScope content( + "InspectorEmptyState", + UI::InspectorPanelContentPadding(), + ImGuiChildFlags_None); if (!content.IsOpen()) { return; } @@ -120,6 +132,8 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen } if (section.open) { + ImGui::Indent(section.contentIndent); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::InspectorComponentControlSpacing()); if (editor) { if (editor->Render(component, &m_context->GetUndoManager())) { m_context->GetSceneManager().MarkSceneDirty(); @@ -127,8 +141,9 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen } else { UI::DrawHintText("No registered editor for this component"); } + ImGui::PopStyleVar(); - UI::EndComponentSection(); + UI::EndComponentSection(section); } } diff --git a/editor/src/panels/ProjectPanel.cpp b/editor/src/panels/ProjectPanel.cpp index 837f70e5..f7b86791 100644 --- a/editor/src/panels/ProjectPanel.cpp +++ b/editor/src/panels/ProjectPanel.cpp @@ -21,14 +21,15 @@ void DrawProjectFolderTreePrefix(const UI::TreeNodePrefixContext& context) { return; } - const float iconWidth = 14.0f; - const float iconHeight = 11.0f; - const float minX = context.min.x + ((context.max.x - context.min.x) - iconWidth) * 0.5f; - const float minY = context.min.y + ((context.max.y - context.min.y) - iconHeight) * 0.5f; + const float width = context.max.x - context.min.x; + const float height = context.max.y - context.min.y; + const float iconExtent = UI::NavigationTreeIconSize(); + const float minX = context.min.x + (width - iconExtent) * 0.5f; + const float minY = context.min.y + (height - iconExtent) * 0.5f; UI::DrawAssetIcon( context.drawList, ImVec2(minX, minY), - ImVec2(minX + iconWidth, minY + iconHeight), + ImVec2(minX + iconExtent, minY + iconExtent), UI::AssetIconKind::Folder); } @@ -52,8 +53,10 @@ void ProjectPanel::Render() { auto& manager = m_context->GetProjectManager(); RenderToolbar(); + ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserSurfaceColor()); UI::PanelContentScope content("ProjectContent", ImVec2(0.0f, 0.0f)); if (!content.IsOpen()) { + ImGui::PopStyleColor(); return; } @@ -63,7 +66,7 @@ void ProjectPanel::Render() { const float clampedNavigationWidth = std::clamp( m_navigationWidth, UI::ProjectNavigationMinWidth(), - std::max(UI::ProjectNavigationMinWidth(), availableWidth - UI::ProjectBrowserMinWidth() - splitterWidth)); + (std::max)(UI::ProjectNavigationMinWidth(), availableWidth - UI::ProjectBrowserMinWidth() - splitterWidth)); m_navigationWidth = clampedNavigationWidth; RenderFolderTreePane(manager); @@ -76,10 +79,18 @@ void ProjectPanel::Render() { RenderBrowserPane(manager); Actions::DrawProjectCreateFolderDialog(*m_context, m_createFolderDialog); + ImGui::PopStyleColor(); } void ProjectPanel::RenderToolbar() { - UI::PanelToolbarScope toolbar("ProjectToolbar", UI::ProjectPanelToolbarHeight()); + UI::PanelToolbarScope toolbar( + "ProjectToolbar", + UI::ProjectPanelToolbarHeight(), + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse, + true, + UI::ToolbarPadding(), + UI::ToolbarItemSpacing(), + UI::ProjectPanelToolbarBackgroundColor()); if (!toolbar.IsOpen()) { return; } @@ -113,6 +124,7 @@ void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) { const AssetItemPtr rootFolder = manager.GetRootFolder(); const AssetItemPtr currentFolder = manager.GetCurrentFolder(); const std::string currentFolderPath = currentFolder ? currentFolder->fullPath : std::string(); + UI::ResetTreeLayout(); if (rootFolder) { RenderFolderTreeNode(manager, rootFolder, currentFolderPath); } else { @@ -143,6 +155,7 @@ void ProjectPanel::RenderFolderTreeNode( nodeDefinition.options.leaf = !hasChildFolders; nodeDefinition.options.defaultOpen = IsCurrentTreeBranch(currentFolderPath, folder->fullPath); nodeDefinition.persistenceKey = folder->fullPath; + nodeDefinition.style = UI::ProjectFolderTreeStyle(); nodeDefinition.prefix.width = UI::NavigationTreePrefixWidth(); nodeDefinition.prefix.draw = DrawProjectFolderTreePrefix; nodeDefinition.callbacks.onInteraction = [this, &manager, folder](const UI::TreeNodeResult& node) { @@ -195,10 +208,12 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { RenderBrowserHeader(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); ImGui::PopStyleVar(2); + ImGui::PopStyleColor(); if (!bodyOpen) { ImGui::EndChild(); ImGui::EndChild(); @@ -244,10 +259,10 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { } } - if (visibleItems.empty()) { + if (visibleItems.empty() && !search.empty()) { UI::DrawEmptyState( - search.empty() ? "No Assets" : "No Search Results", - search.empty() ? "Current folder is empty" : "No assets match the current search"); + "No Search Results", + "No assets match the current search"); } Actions::HandleProjectBackgroundPrimaryClick(manager); @@ -286,21 +301,23 @@ void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) { 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(); + const float rowHeight = UI::BreadcrumbItemHeight(); + const float startY = ImGui::GetCursorPosY(); + const float availableHeight = ImGui::GetContentRegionAvail().y; + if (availableHeight > rowHeight) { + ImGui::SetCursorPosY(startY + (availableHeight - rowHeight) * 0.5f); } + UI::DrawToolbarBreadcrumbs( + "Assets", + manager.GetPathDepth(), + [&](size_t index) { return manager.GetPathName(index); }, + [&](size_t index) { manager.NavigateToIndex(index); }); + 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); + const ImVec2 windowMin = ImGui::GetWindowPos(); + const ImVec2 windowMax(windowMin.x + ImGui::GetWindowSize().x, windowMin.y + ImGui::GetWindowSize().y); + UI::DrawHorizontalDivider(drawList, windowMin.x, windowMax.x, windowMax.y - 0.5f); ImGui::EndChild(); } @@ -311,6 +328,13 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem 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; + UI::AssetTileOptions tileOptions; + tileOptions.drawIdleFrame = false; + tileOptions.drawSelectionBorder = false; + if (item->isFolder) { + tileOptions.iconOffset = UI::FolderAssetTileIconOffset(); + tileOptions.iconSize = UI::FolderAssetTileIconSize(); + } const UI::AssetTileResult tile = UI::DrawAssetTile( item->name.c_str(), @@ -318,7 +342,8 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem isDraggingThisItem, [&](ImDrawList* drawList, const ImVec2& iconMin, const ImVec2& iconMax) { UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind); - }); + }, + tileOptions); if (tile.clicked) { interaction.clicked = true;