feat: update editor ui framework and assets
@@ -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
|
||||
)
|
||||
|
||||
|
||||
BIN
editor/project.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
28
editor/rename_color.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def get_dominant_color(image_path):
|
||||
img = Image.open(image_path).convert("RGB")
|
||||
img = img.resize((1, 1), Image.Resampling.LANCZOS)
|
||||
r, g, b = img.getpixel((0, 0))
|
||||
return r, g, b
|
||||
|
||||
|
||||
def rename_with_color(base_path):
|
||||
files = ["color.png", "color2.png"]
|
||||
for f in files:
|
||||
old_path = os.path.join(base_path, f)
|
||||
if os.path.exists(old_path):
|
||||
r, g, b = get_dominant_color(old_path)
|
||||
new_name = f"color-({r},{g},{b}).png"
|
||||
new_path = os.path.join(base_path, new_name)
|
||||
os.rename(old_path, new_path)
|
||||
print(f"Renamed: {f} -> {new_name}")
|
||||
else:
|
||||
print(f"File not found: {old_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
base = r"D:\Xuanchi\Main\XCEngine\editor"
|
||||
rename_with_color(base)
|
||||
BIN
editor/resources/Icons/app.ico
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
editor/resources/Icons/folder_empty_icon.png
Normal file
|
After Width: | Height: | Size: 353 B |
BIN
editor/resources/Icons/folder_icon.png
Normal file
|
After Width: | Height: | Size: 270 B |
BIN
editor/resources/Icons/gameobject_icon.png
Normal file
|
After Width: | Height: | Size: 741 B |
BIN
editor/resources/Icons/logo_icon.png
Normal file
|
After Width: | Height: | Size: 926 B |
25
editor/resources/Icons/resize_png.py
Normal file
@@ -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 <input.png> [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)
|
||||
|
Before Width: | Height: | Size: 6.5 KiB |
@@ -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) {
|
||||
|
||||
@@ -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<int>(items.size())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return items[selectedIndex];
|
||||
return context.GetProjectManager().GetSelectedItem();
|
||||
}
|
||||
|
||||
} // namespace Actions
|
||||
|
||||
@@ -97,7 +97,6 @@ inline void DrawMainMenuBar(IEditorContext& context, UI::DeferredPopupState& abo
|
||||
UI::DrawMenuScope("Help", [&]() {
|
||||
DrawHelpMenuActions(aboutPopup);
|
||||
});
|
||||
UI::DrawSceneStatusWidget(context);
|
||||
ImGui::EndMainMenuBar();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<int>(i);
|
||||
}
|
||||
|
||||
if (items[i] && items[i]->fullPath == item->fullPath) {
|
||||
return static_cast<int>(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<const char*>(payload->Data);
|
||||
accepted = Commands::MoveAssetToFolder(projectManager, draggedPath, targetFolder);
|
||||
const char* payloadPath = static_cast<const char*>(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<AssetItemPtr>& 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <windows.h>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -12,9 +12,16 @@ class IProjectManager {
|
||||
public:
|
||||
virtual ~IProjectManager() = default;
|
||||
|
||||
virtual std::vector<AssetItemPtr>& GetCurrentItems() = 0;
|
||||
virtual const std::vector<AssetItemPtr>& 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
|
||||
} // namespace XCEngine
|
||||
|
||||
3
editor/src/EditorApp.rc
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "EditorResources.h"
|
||||
|
||||
IDI_APP_ICON ICON "../resources/Icons/app.ico"
|
||||
3
editor/src/EditorResources.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#define IDI_APP_ICON 101
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "Core/EventBus.h"
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "UI/DockHostStyle.h"
|
||||
#include "UI/DockTabBarChrome.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "ProjectManager.h"
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
#include <fstream>
|
||||
#include <windows.h>
|
||||
|
||||
@@ -9,23 +10,104 @@ namespace fs = std::filesystem;
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
std::vector<AssetItemPtr>& 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<AssetItemPtr>& ProjectManager::GetCurrentItems() const {
|
||||
if (m_path.empty()) {
|
||||
static std::vector<AssetItemPtr> empty;
|
||||
static const std::vector<AssetItemPtr> 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<int>(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<int>(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<AssetItemPtr> 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<AssetItem>();
|
||||
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<AssetItem>();
|
||||
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<AssetItemPtr>& 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<std::string> 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<AssetItemPtr> 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<AssetItem>();
|
||||
item->name = WstringToUtf8(nameW);
|
||||
|
||||
@@ -11,9 +11,16 @@ namespace Editor {
|
||||
|
||||
class ProjectManager : public IProjectManager {
|
||||
public:
|
||||
std::vector<AssetItemPtr>& GetCurrentItems() override;
|
||||
int GetSelectedIndex() const override { return m_selectedIndex; }
|
||||
void SetSelectedIndex(int index) override { m_selectedIndex = index; }
|
||||
const std::vector<AssetItemPtr>& 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<AssetItemPtr>& 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<AssetItemPtr> m_path;
|
||||
int m_selectedIndex = -1;
|
||||
std::string m_selectedItemPath;
|
||||
std::string m_projectPath;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <d3d12.h>
|
||||
#include <dxgi1_6.h>
|
||||
#include <array>
|
||||
#include <windows.h>
|
||||
|
||||
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<bool, kSrvDescriptorCount> m_srvDescriptorUsage = {};
|
||||
};
|
||||
|
||||
} // namespace Platform
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Application.h"
|
||||
#include "EditorResources.h"
|
||||
#include "UI/ImGuiBackendBridge.h"
|
||||
|
||||
#include <windows.h>
|
||||
@@ -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<int>(LOWORD(lParam)), static_cast<int>(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<HICON>(LoadImageW(hInstance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
|
||||
wc.hIconSm = static_cast<HICON>(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<LPARAM>(wc.hIcon));
|
||||
}
|
||||
if (wc.hIconSm) {
|
||||
SendMessageW(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(wc.hIconSm));
|
||||
}
|
||||
|
||||
ShowWindow(hwnd, nCmdShow);
|
||||
UpdateWindow(hwnd);
|
||||
|
||||
|
||||
375
editor/src/UI/BuiltInIcons.cpp
Normal file
@@ -0,0 +1,375 @@
|
||||
#include "BuiltInIcons.h"
|
||||
|
||||
#include "ImGuiBackendBridge.h"
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "StyleTokens.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <stb_image.h>
|
||||
|
||||
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<ID3D12Resource> 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<ID3D12Fence> 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<UINT>(width * 4);
|
||||
|
||||
D3D12_RESOURCE_DESC textureDesc = {};
|
||||
textureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
|
||||
textureDesc.Alignment = 0;
|
||||
textureDesc.Width = static_cast<UINT64>(width);
|
||||
textureDesc.Height = static_cast<UINT>(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<ID3D12Resource> 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<ID3D12Resource> 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<void**>(&mappedData)))) {
|
||||
stbi_image_free(pixels);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (UINT row = 0; row < numRows; ++row) {
|
||||
std::memcpy(
|
||||
mappedData + footprint.Offset + static_cast<SIZE_T>(row) * footprint.Footprint.RowPitch,
|
||||
pixels + static_cast<size_t>(row) * srcRowPitch,
|
||||
srcRowPitch);
|
||||
}
|
||||
uploadResource->Unmap(0, nullptr);
|
||||
stbi_image_free(pixels);
|
||||
|
||||
ComPtr<ID3D12CommandAllocator> commandAllocator;
|
||||
ComPtr<ID3D12GraphicsCommandList> 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<intptr_t>(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<float>(texture.width),
|
||||
availableHeight / static_cast<float>(texture.height));
|
||||
return ImVec2(
|
||||
static_cast<float>(texture.width) * scale,
|
||||
static_cast<float>(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
|
||||
31
editor/src/UI/BuiltInIcons.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
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
|
||||
@@ -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 <typename DrawControlFn>
|
||||
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) {
|
||||
|
||||
@@ -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<int>(std::distance(order.begin(), it));
|
||||
destinationIndex = std::clamp(destinationIndex, 0, static_cast<int>(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<ImRect>& tabRects,
|
||||
ImGuiID draggedTabId,
|
||||
int sourceIndex) {
|
||||
if (sourceIndex < 0 || sourceIndex >= static_cast<int>(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<int>(tabRects.size()) - 1;
|
||||
for (int i = 0; i < static_cast<int>(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<ImRect>& 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<int>(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<int>(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<ImGuiWindow*> orderedWindows = GetOrderedDockWindows(node);
|
||||
float selectedTabMinX = stripMin.x;
|
||||
float selectedTabMaxX = stripMin.x;
|
||||
bool hasSelectedTab = false;
|
||||
std::vector<ImRect> 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<int>(orderedWindows.size()) && i < static_cast<int>(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;
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <d3d12.h>
|
||||
#include <dxgi1_6.h>
|
||||
#include <imgui.h>
|
||||
#include <imgui_impl_dx12.h>
|
||||
#include <imgui_impl_win32.h>
|
||||
#include <vector>
|
||||
#include <windows.h>
|
||||
|
||||
#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<ImGuiBackendBridge*>(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<ImGuiBackendBridge*>(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<SIZE_T>(i) * m_srvDescriptorSize;
|
||||
outGpuHandle->ptr = m_srvGpuStart.ptr + static_cast<UINT64>(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<size_t>(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<bool> m_srvUsage;
|
||||
};
|
||||
|
||||
} // namespace UI
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Core.h"
|
||||
#include "DockTabBarChrome.h"
|
||||
#include "StyleTokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace UI {
|
||||
|
||||
inline PropertyLayoutSpec InspectorPropertyLayout() {
|
||||
return MakePropertyLayout();
|
||||
}
|
||||
|
||||
template <typename ApplyFn>
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
128
editor/src/UI/PropertyLayout.h
Normal file
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
#include "StyleTokens.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <imgui.h>
|
||||
#include <utility>
|
||||
|
||||
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 <typename DrawControlFn>
|
||||
inline auto DrawPropertyRow(
|
||||
const char* label,
|
||||
const PropertyLayoutSpec& spec,
|
||||
DrawControlFn&& drawControl) -> decltype(drawControl(std::declval<const PropertyLayoutMetrics&>())) {
|
||||
using Result = decltype(drawControl(std::declval<const PropertyLayoutMetrics&>()));
|
||||
|
||||
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
|
||||
@@ -1,35 +1,165 @@
|
||||
#pragma once
|
||||
|
||||
#include "Core.h"
|
||||
#include "PropertyLayout.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <imgui.h>
|
||||
|
||||
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<float>(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<float>(value - min) / static_cast<float>(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<int>(std::round(mouseRatio * static_cast<float>(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;
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "StyleTokens.h"
|
||||
#include "Core.h"
|
||||
|
||||
#include <functional>
|
||||
#include <imgui.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
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<float>& TreeIndentStack() {
|
||||
static std::vector<float> 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<float>(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<int>(reserveWidth / (spaceWidth > 0.0f ? spaceWidth : 1.0f)) + 2;
|
||||
if (spaceCount < 1) {
|
||||
spaceCount = 1;
|
||||
}
|
||||
|
||||
displayLabel.insert(0, static_cast<size_t>(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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Core.h"
|
||||
#include "PropertyLayout.h"
|
||||
#include <imgui.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <typename DrawContentFn>
|
||||
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 <typename GetNameFn, typename NavigateFn>
|
||||
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<int>(i));
|
||||
if (DrawBreadcrumbSegment(label.c_str(), !current, current)) {
|
||||
navigateToSegment(i);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
template <typename DrawIconFn>
|
||||
@@ -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 <typename DrawMenuFn>
|
||||
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)) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "Actions/ConsoleActionRouter.h"
|
||||
#include "Actions/ActionRouting.h"
|
||||
#include "ConsolePanel.h"
|
||||
#include "Core/EditorConsoleSink.h"
|
||||
#include "UI/UI.h"
|
||||
#include <imgui.h>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||