feat: update editor ui framework and assets

This commit is contained in:
2026-03-28 15:07:19 +08:00
parent 4a12e26860
commit 4717b595c4
45 changed files with 2434 additions and 461 deletions

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

28
editor/rename_color.py Normal file
View 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

View 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -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) {

View File

@@ -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

View File

@@ -97,7 +97,6 @@ inline void DrawMainMenuBar(IEditorContext& context, UI::DeferredPopupState& abo
UI::DrawMenuScope("Help", [&]() {
DrawHelpMenuActions(aboutPopup);
});
UI::DrawSceneStatusWidget(context);
ImGui::EndMainMenuBar();
}

View File

@@ -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);
});
}

View File

@@ -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();
}

View File

@@ -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;
};
}

View File

@@ -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(

View File

@@ -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
View File

@@ -0,0 +1,3 @@
#include "EditorResources.h"
IDI_APP_ICON ICON "../resources/Icons/app.ico"

View File

@@ -0,0 +1,3 @@
#pragma once
#define IDI_APP_ICON 101

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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;
};
}
}
}

View File

@@ -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

View File

@@ -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);

View 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

View 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

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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);
}
}

View 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

View File

@@ -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", &currentItem, 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;
});

View File

@@ -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() {

View File

@@ -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

View File

@@ -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"

View File

@@ -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);
});
}

View File

@@ -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)) {

View File

@@ -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);
}
}

View File

@@ -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;
};
}

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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;