From 4717b595c40100e59ff9bca5dff57db72c42b321 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 28 Mar 2026 15:07:19 +0800 Subject: [PATCH] feat: update editor ui framework and assets --- editor/CMakeLists.txt | 3 + editor/project.png | Bin 0 -> 30046 bytes editor/rename_color.py | 28 ++ editor/resources/Icons/app.ico | Bin 0 -> 30290 bytes editor/resources/Icons/folder_empty_icon.png | Bin 0 -> 353 bytes editor/resources/Icons/folder_icon.png | Bin 0 -> 270 bytes editor/resources/Icons/gameobject_icon.png | Bin 0 -> 741 bytes editor/resources/Icons/logo_icon.png | Bin 0 -> 926 bytes editor/resources/Icons/resize_png.py | 25 ++ editor/resources/Icons/简单模型.png | Bin 6639 -> 0 bytes editor/src/Actions/EditActionRouter.h | 5 +- editor/src/Actions/EditorActions.h | 9 +- editor/src/Actions/MainMenuActionRouter.h | 1 - editor/src/Actions/ProjectActionRouter.h | 49 +-- editor/src/Application.cpp | 39 +- editor/src/Application.h | 2 + editor/src/Commands/ProjectCommands.h | 15 +- editor/src/Core/IProjectManager.h | 13 +- editor/src/EditorApp.rc | 3 + editor/src/EditorResources.h | 3 + editor/src/Layout/DockLayoutController.h | 3 + editor/src/Managers/ProjectManager.cpp | 250 +++++++++++-- editor/src/Managers/ProjectManager.h | 23 +- editor/src/Platform/D3D12WindowRenderer.h | 23 +- editor/src/Platform/Win32EditorHost.h | 32 ++ editor/src/UI/BuiltInIcons.cpp | 375 +++++++++++++++++++ editor/src/UI/BuiltInIcons.h | 31 ++ editor/src/UI/Core.h | 236 +++++++++--- editor/src/UI/DockTabBarChrome.h | 190 +++++++++- editor/src/UI/ImGuiBackendBridge.h | 124 +++++- editor/src/UI/ImGuiSession.h | 61 ++- editor/src/UI/PanelChrome.h | 29 +- editor/src/UI/PropertyGrid.h | 26 +- editor/src/UI/PropertyLayout.h | 128 +++++++ editor/src/UI/ScalarControls.h | 244 ++++++++++-- editor/src/UI/StyleTokens.h | 308 ++++++++++++++- editor/src/UI/TreeView.h | 177 ++++++--- editor/src/UI/UI.h | 2 + editor/src/UI/VectorControls.h | 22 +- editor/src/UI/Widgets.h | 248 ++++++++---- editor/src/panels/ConsolePanel.cpp | 18 - editor/src/panels/ConsolePanel.h | 4 - editor/src/panels/HierarchyPanel.cpp | 22 +- editor/src/panels/InspectorPanel.cpp | 53 ++- editor/src/panels/ProjectPanel.cpp | 71 ++-- 45 files changed, 2434 insertions(+), 461 deletions(-) create mode 100644 editor/project.png create mode 100644 editor/rename_color.py create mode 100644 editor/resources/Icons/app.ico create mode 100644 editor/resources/Icons/folder_empty_icon.png create mode 100644 editor/resources/Icons/folder_icon.png create mode 100644 editor/resources/Icons/gameobject_icon.png create mode 100644 editor/resources/Icons/logo_icon.png create mode 100644 editor/resources/Icons/resize_png.py delete mode 100644 editor/resources/Icons/简单模型.png create mode 100644 editor/src/EditorApp.rc create mode 100644 editor/src/EditorResources.h create mode 100644 editor/src/UI/BuiltInIcons.cpp create mode 100644 editor/src/UI/BuiltInIcons.h create mode 100644 editor/src/UI/PropertyLayout.h 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 0000000000000000000000000000000000000000..77a0c04fc00f108bd58761bce2cf186e4d6b94e2 GIT binary patch literal 30046 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

j-WMCSJFI%&{hQ^(J1<9;`_X%orUd?1@3yG;O-< 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 {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 0000000000000000000000000000000000000000..ecec95103812c35989dcf460e6cb1cfa114b5263 GIT binary patch literal 30290 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 literal 0 HcmV?d00001 diff --git a/editor/resources/Icons/folder_empty_icon.png b/editor/resources/Icons/folder_empty_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..247b25a468357d49db8fd55b7a81fc8b605ae0bb GIT binary patch literal 353 zcmV-n0iOPeP)kdg0003fNkl}U?WH#LTv4{vaz@qa0MH?wA#rX2PYVE zaD|t(xFok1?=Fj4SmdvUot>ZmpV=K~@oR`#sC+!Mk<$!h-bAogl^+rl%;hGWacikS z`8*QQuM}up)_Gx1zS=492)S~PscmC4S@zUJvlIXa?UnKpAYJOX0%Y9 z$+T`EArW0G z4T234vN;PfB_^@9kPw#}VbxMyuE1C*E&c#sz&KC0xY%>D00000NkvXXu0mjf%^Q>e literal 0 HcmV?d00001 diff --git a/editor/resources/Icons/folder_icon.png b/editor/resources/Icons/folder_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..55d061ce19eb02667d2109f894c953c78360c6ba GIT binary patch literal 270 zcmV+p0rCEcP)kdg0002hNkl7G6M!b!>XQf4-X;51gEfQB5;M1& literal 0 HcmV?d00001 diff --git a/editor/resources/Icons/gameobject_icon.png b/editor/resources/Icons/gameobject_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..39f08e89b074b550419b718a56369805104a511f GIT binary patch literal 741 zcmVkdg00084NklLw8 zVr!%gDa33HE>x6Y(u#Cb{Fi7nnmp&;B>p6GPZz<|naspdP`~B*c{txW?{_}weV?F` zW2$y`t*@7Yq4d>dGp`A-dbN0)T9lQ927)dPgi)0MTcK9psSQf$wgx1%XoU*89Gp;Z zczaR6RBCpe@6dN@R-)nHSM`K9C2Q7J|6c1>R)G9On*)Y~yroxw}r++C~m>2WOUU%N02}i1Wbq^YN>O#;3NA ze{lQpg{4Na25pg4r`5rS&x6GQ*&8~qyOM9VhX1KKMwsO(4ji@hUI8+hvd`@};5Z}X z<|~kt(zx>K(GqodfAouewjeoeUlfk}Y2U3t{hBNqKRHGjrD;0NH}vuZ=brs!E9aAI zt;J3fNN|S8)A(9KwI=QGcCD0jEjXs`@X;G6-9mjL=~ifO^>_Xc X>q)hk2U|pJ00000NkvXXu0mjfW;$lV literal 0 HcmV?d00001 diff --git a/editor/resources/Icons/logo_icon.png b/editor/resources/Icons/logo_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b9760f6d744f1108c4774a496512e8d341253bb3 GIT binary patch literal 926 zcmV;P17ZA$P)%O0Ybzaiw5Lqb*% zJm}F~JR~007%&LJq6D&ziQ{hfR9C-O?^X4a>ggKW*t;2%;AtNWHPrk1z0c|-*`{s! zzeq%sh;+kmadELbfqX|qda|_CtM%fk9vMi|dcP1HcDB-;1b7l10Kh>Jd4Wh?G{!t% z^E#UuG{zi7#50I^2?4IKwda}f^(Ygfe~{x!AkVY+MMS*ga0ukt2Q|Ofl-t30j=vUf+|BIqg0CP1cJ0fPHJj=2}HGe9cS(Y7Vrn@76u5#p}|bH`j7 zGkm*gyOkCes=BE(O`l}4x4m;t%2E*EJCR#E0`fFXpCW7jEWMY~J0WZDA>!F0%TDBZ ze%KbpJJ#B3wPP0m1)T>*9+7FSU-sUGs4*BZMt>Ua5#UB0X~|4Ci}m#bZ2=(S#UMw7 z-wy_Z{q?kL-RZHO0Onj>FGc{w!h91WVWN9xFxVe;vMk#pA~Q`pM8*8d%1TupnPj{w zbW2OU(3uuPnnf%^RX>YZb)DAfsVGZCr@i+B@BFU}a5CtV<%8s6-`+RE*Ng?_()|iG!JlVx27% zUsfrGkm@pwjPu@0nhsvwEY(f@D3kmbr^e`yV{=&qi$(EEm|O3hC$e{j!{7Ecbawaq z{h4N&i0Gv-niDx6Z?KJxBTPh0WUCDZ<`b>;J|;>-SC8sxuZO!fI{@ah8gyT4Jy+Mf zy|j_Jwzjs@Lh)0W)vzcC;Rg|Uq5_oXZ`1}W=Va||0QfK%2Y{y$&6{N5>w4D1*daiA zu$hTXqcu9aFfwk@F{9S{xe@=?j@tRV&N-^u*pId(0)$zIf2>UBycrJPYTDk^X_4pe zHI=eWBL%Nz$6d*A<%0x7#GX( z@^Ye-a&3w+19ch$g3)&Q`+xaRk#5sAC67pd0S}P({iWlJ!vFvP07*qoM6N<$f{G}( Awg3PC literal 0 HcmV?d00001 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 65120ae01d7ef082c18af9e4e18bb79c04027ece..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6639 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 Rgd45}prc`^UaRJa_#d<6JwE^d 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;