diff --git a/ui_editor/CMakeLists.txt b/ui_editor/CMakeLists.txt new file mode 100644 index 00000000..4fdcecdb --- /dev/null +++ b/ui_editor/CMakeLists.txt @@ -0,0 +1,66 @@ +cmake_minimum_required(VERSION 3.15) +project(XCVolumeRendererUI2 VERSION 1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_definitions(-DUNICODE -D_UNICODE) +add_definitions(-DIMGUI_ENABLE_DOCKING) + +include(FetchContent) + +FetchContent_Declare( + imgui + GIT_REPOSITORY https://gitee.com/mirrors/imgui.git + GIT_TAG docking + GIT_SHALLOW TRUE +) + +FetchContent_MakeAvailable(imgui) + +set(IMGUI_SOURCES + ${imgui_SOURCE_DIR}/imgui.cpp + ${imgui_SOURCE_DIR}/imgui_demo.cpp + ${imgui_SOURCE_DIR}/imgui_draw.cpp + ${imgui_SOURCE_DIR}/imgui_tables.cpp + ${imgui_SOURCE_DIR}/imgui_widgets.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_win32.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_dx12.cpp +) + +add_executable(${PROJECT_NAME} WIN32 + src/main.cpp + src/Application.cpp + src/Theme.cpp + src/Managers/SceneManager.cpp + src/Managers/LogSystem.cpp + src/Managers/ProjectManager.cpp + src/panels/Panel.cpp + src/panels/MenuBar.cpp + src/panels/HierarchyPanel.cpp + src/panels/SceneViewPanel.cpp + src/panels/GameViewPanel.cpp + src/panels/InspectorPanel.cpp + src/panels/ConsolePanel.cpp + src/panels/ProjectPanel.cpp + ${IMGUI_SOURCES} +) + +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${imgui_SOURCE_DIR} + ${imgui_SOURCE_DIR}/backends +) + +target_compile_definitions(${PROJECT_NAME} PRIVATE UNICODE _UNICODE) +target_compile_options(${PROJECT_NAME} PRIVATE /utf-8 /MT) + +target_link_libraries(${PROJECT_NAME} PRIVATE + d3d12.lib + dxgi.lib + d3dcompiler.lib +) + +set_target_properties(${PROJECT_NAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin" +) \ No newline at end of file diff --git a/ui_editor/README.md b/ui_editor/README.md new file mode 100644 index 00000000..55e935f7 --- /dev/null +++ b/ui_editor/README.md @@ -0,0 +1,183 @@ +# UI Editor + +Unity 风格的编辑器 UI,使用 ImGui 实现,作为 XCEngine 游戏引擎编辑器的一部分。 + +## 简介 + +XCGameEngine UI 是一个仿 Unity 编辑器的桌面应用程序,提供场景管理、层级视图、属性检查器等功能。 + +## 技术栈 + +- **渲染 API**: DirectX 12 +- **UI 框架**: ImGui +- **语言**: C++17 +- **构建系统**: CMake +- **依赖库**: DirectX 12 SDK + +## 项目结构 + +``` +ui/ +├── src/ +│ ├── main.cpp # 程序入口 +│ ├── Application.cpp/h # 应用主类 +│ ├── Theme.cpp/h # 主题系统 +│ ├── Core/ +│ │ ├── GameObject.h # 游戏对象 +│ │ └── LogEntry.h # 日志条目 +│ ├── Managers/ +│ │ ├── LogSystem.cpp/h # 日志系统 +│ │ ├── ProjectManager.cpp/h # 项目管理 +│ │ ├── SceneManager.cpp/h # 场景管理 +│ │ └── SelectionManager.cpp/h # 选择管理 +│ └── panels/ +│ ├── Panel.cpp/h # 面板基类 +│ ├── MenuBar.cpp/h # 菜单栏 +│ ├── HierarchyPanel.cpp/h # 层级面板 +│ ├── InspectorPanel.cpp/h # 检查器面板 +│ ├── SceneViewPanel.cpp/h # 场景视图 +│ ├── GameViewPanel.cpp/h # 游戏视图 +│ ├── ProjectPanel.cpp/h # 项目面板 +│ └── ConsolePanel.cpp/h # 控制台面板 +├── bin/Release/ # 输出目录 +│ ├── XCVolumeRendererUI2.exe # 可执行文件 +│ ├── imgui.ini # ImGui 配置 +│ └── Assets/ +│ └── Models/ +│ └── Character.fbx # 示例模型 +├── build/ # 构建目录 +└── CMakeLists.txt # CMake 配置 +``` + +## 构建方法 + +### 前置要求 + +- Windows 10/11 +- Visual Studio 2019 或更高版本 +- CMake 3.15+ + +### 构建步骤 + +```bash +cd ui +mkdir build && cd build +cmake .. +cmake --build . --config Release +``` + +### 运行 + +```bash +# 运行编译好的可执行文件 +.\bin\Release\XCGameEngineUI.exe +``` + +## 功能特性 + +### 编辑器面板 + +#### 菜单栏(MenuBar) +- 文件菜单(新建、打开、保存等) +- 编辑菜单(撤销、重做等) +- 视图菜单(面板显示/隐藏) +- 帮助菜单 + +#### 层级面板(Hierarchy Panel) +- 显示场景中所有游戏对象 +- 树形结构展示父子关系 +- 支持对象选择 +- 对象重命名 + +#### 检查器面板(Inspector Panel) +- 显示选中对象的属性 +- 支持组件编辑 +- 变换组件(位置、旋转、缩放) +- 材质组件 + +#### 场景视图(Scene View) +- 3D 场景预览 +- 相机控制(平移、旋转、缩放) +- 对象选择 +- 辅助工具(网格、轴心) + +#### 游戏视图(Game View) +- 游戏运行时的画面预览 +- 分辨率设置 +- 宽高比选择 + +#### 项目面板(Project Panel) +- 项目文件浏览器 +- 资源组织 +- 搜索过滤 + +#### 控制台面板(Console Panel) +- 日志输出 +- 警告和错误显示 +- 日志级别过滤 +- 清空日志 + +### 管理系统 + +#### 日志系统(LogSystem) +- 分级日志(Info、Warning、Error) +- 时间戳 +- 日志持久化 + +#### 项目管理(ProjectManager) +- 项目创建/打开 +- 资源路径管理 + +#### 场景管理(SceneManager) +- 场景加载/保存 +- 对象生命周期管理 + +#### 选择管理(SelectionManager) +- 当前选中对象追踪 +- 多选支持 + +### 主题系统 + +- 深色主题(Dark Theme) +- 可自定义配色方案 + +## 窗口布局 + +默认布局采用经典的 Unity 编辑器风格: + +``` ++----------------------------------------------------------+ +| 菜单栏 | ++----------+------------------------+----------------------+ +| | | | +| 项目 | 场景视图 | 检查器 | +| 面板 | | | +| | | | ++----------+------------------------+----------------------+ +| 层级面板 | 游戏视图 | +| | | ++------------------------------------+----------------------+ +| 控制台面板 | ++----------------------------------------------------------+ +``` + +## 依赖说明 + +- ImGui - 跨平台 GUI 库 +- DirectX 12 - 渲染 API +- Windows SDK - 窗口管理 + +## 扩展开发 + +### 添加新面板 + +1. 在 `panels/` 目录下创建新的面板类 +2. 继承 `Panel` 基类 +3. 实现 `Render()` 方法 +4. 在 `Application` 中注册新面板 + +### 添加新组件 + +1. 定义组件类 +2. 在 `GameObject` 中注册组件类型 +3. 在 `InspectorPanel` 中添加属性编辑器 diff --git a/ui_editor/src/Application.cpp b/ui_editor/src/Application.cpp new file mode 100644 index 00000000..47074823 --- /dev/null +++ b/ui_editor/src/Application.cpp @@ -0,0 +1,303 @@ +#include "Application.h" +#include +#include +#include +#include + +extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +namespace UI { + +Application& Application::Get() { + static Application instance; + return instance; +} + +bool Application::Initialize(HWND hwnd) { + m_hwnd = hwnd; + + if (!CreateDevice()) { + MessageBoxW(hwnd, L"Failed to create D3D12 device", L"Error", MB_OK | MB_ICONERROR); + return false; + } + if (!CreateRenderTarget()) { + MessageBoxW(hwnd, L"Failed to create render target", L"Error", MB_OK | MB_ICONERROR); + return false; + } + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + + io.Fonts->AddFontFromFileTTF("C:/Windows/Fonts/msyh.ttc", 16.0f); + io.Fonts->AddFontDefault(); + + unsigned char* pixels; + int width, height; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + + ApplyUnityDarkTheme(); + + ImGui_ImplWin32_Init(hwnd); + ImGui_ImplDX12_Init(m_device, 3, DXGI_FORMAT_R8G8B8A8_UNORM, m_srvHeap, + m_srvHeap->GetCPUDescriptorHandleForHeapStart(), + m_srvHeap->GetGPUDescriptorHandleForHeapStart()); + + m_menuBar = std::make_unique(); + m_hierarchyPanel = std::make_unique(); + m_sceneViewPanel = std::make_unique(); + m_gameViewPanel = std::make_unique(); + m_inspectorPanel = std::make_unique(); + m_consolePanel = std::make_unique(); + m_projectPanel = std::make_unique(); + + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(nullptr, exePath, MAX_PATH); + std::wstring exeDirW(exePath); + size_t pos = exeDirW.find_last_of(L"\\/"); + if (pos != std::wstring::npos) { + exeDirW = exeDirW.substr(0, pos); + } + std::string exeDir; + int len = WideCharToMultiByte(CP_UTF8, 0, exeDirW.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (len > 0) { + exeDir.resize(len - 1); + WideCharToMultiByte(CP_UTF8, 0, exeDirW.c_str(), -1, &exeDir[0], len, nullptr, nullptr); + } + m_projectPanel->Initialize(exeDir); + + return true; +} + +void Application::Shutdown() { + ImGui_ImplDX12_Shutdown(); + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); + + CleanupRenderTarget(); + + if (m_fence) m_fence->Release(); + if (m_commandList) m_commandList->Release(); + if (m_commandAllocator) m_commandAllocator->Release(); + if (m_commandQueue) m_commandQueue->Release(); + if (m_rtvHeap) m_rtvHeap->Release(); + if (m_srvHeap) m_srvHeap->Release(); + if (m_swapChain) m_swapChain->Release(); + if (m_device) m_device->Release(); +} + +void Application::Render() { + ImGui_ImplDX12_NewFrame(); + ImGui_ImplWin32_NewFrame(); + ImGui::NewFrame(); + + SetupDockspace(); + RenderUI(); + + ImGui::Render(); + + m_frameIndex = m_swapChain->GetCurrentBackBufferIndex(); + m_commandAllocator->Reset(); + m_commandList->Reset(m_commandAllocator, nullptr); + + D3D12_RESOURCE_BARRIER barrier = {}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + barrier.Transition.pResource = m_renderTargets[m_frameIndex]; + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + m_commandList->ResourceBarrier(1, &barrier); + + float clearColor[4] = { 0.12f, 0.12f, 0.12f, 1.0f }; + D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = m_rtvHeap->GetCPUDescriptorHandleForHeapStart(); + rtvHandle.ptr += m_frameIndex * m_rtvDescriptorSize; + m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); + m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr); + + ID3D12DescriptorHeap* heaps[] = { m_srvHeap }; + m_commandList->SetDescriptorHeaps(1, heaps); + + ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), m_commandList); + + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; + m_commandList->ResourceBarrier(1, &barrier); + + m_commandList->Close(); + ID3D12CommandList* cmdLists[] = { m_commandList }; + m_commandQueue->ExecuteCommandLists(1, cmdLists); + + m_swapChain->Present(1, 0); + + m_fenceValue++; + m_commandQueue->Signal(m_fence, m_fenceValue); + if (m_fence->GetCompletedValue() < m_fenceValue) { + m_fence->SetEventOnCompletion(m_fenceValue, nullptr); + } +} + +void Application::OnResize(int width, int height) { + if (width <= 0 || height <= 0) return; + + m_width = width; + m_height = height; + + CleanupRenderTarget(); + + if (m_swapChain) { + DXGI_SWAP_CHAIN_DESC desc; + m_swapChain->GetDesc(&desc); + m_swapChain->ResizeBuffers(3, width, height, desc.BufferDesc.Format, desc.Flags); + } + + CreateRenderTarget(); +} + +bool Application::CreateDevice() { + HRESULT hr = D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&m_device)); + if (FAILED(hr)) { + return false; + } + + D3D12_COMMAND_QUEUE_DESC queueDesc = {}; + queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + queueDesc.Priority = 0; + queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; + queueDesc.NodeMask = 0; + hr = m_device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_commandQueue)); + if (FAILED(hr)) return false; + + hr = m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_commandAllocator)); + if (FAILED(hr)) return false; + + hr = m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator, nullptr, IID_PPV_ARGS(&m_commandList)); + if (FAILED(hr)) return false; + m_commandList->Close(); + + hr = m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)); + if (FAILED(hr)) return false; + + IDXGIFactory4* factory = nullptr; + hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory)); + if (FAILED(hr)) return false; + + DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {}; + swapChainDesc.BufferCount = 3; + swapChainDesc.Width = m_width; + swapChainDesc.Height = m_height; + swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + swapChainDesc.SampleDesc.Count = 1; + + IDXGISwapChain1* swapChain1 = nullptr; + hr = factory->CreateSwapChainForHwnd(m_commandQueue, m_hwnd, &swapChainDesc, nullptr, nullptr, &swapChain1); + factory->Release(); + if (FAILED(hr)) return false; + + hr = swapChain1->QueryInterface(IID_PPV_ARGS(&m_swapChain)); + swapChain1->Release(); + if (FAILED(hr)) return false; + + D3D12_DESCRIPTOR_HEAP_DESC rtvDesc = {}; + rtvDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; + rtvDesc.NumDescriptors = 3; + rtvDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + hr = m_device->CreateDescriptorHeap(&rtvDesc, IID_PPV_ARGS(&m_rtvHeap)); + if (FAILED(hr)) return false; + m_rtvDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); + + D3D12_DESCRIPTOR_HEAP_DESC srvDesc = {}; + srvDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + srvDesc.NumDescriptors = 1; + srvDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + hr = m_device->CreateDescriptorHeap(&srvDesc, IID_PPV_ARGS(&m_srvHeap)); + if (FAILED(hr)) return false; + + return true; +} + +bool Application::CreateRenderTarget() { + if (!m_swapChain || !m_device || !m_rtvHeap) return false; + + for (UINT i = 0; i < 3; i++) { + HRESULT hr = m_swapChain->GetBuffer(i, IID_PPV_ARGS(&m_renderTargets[i])); + if (FAILED(hr)) return false; + + D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = m_rtvHeap->GetCPUDescriptorHandleForHeapStart(); + rtvHandle.ptr += i * m_rtvDescriptorSize; + m_device->CreateRenderTargetView(m_renderTargets[i], nullptr, rtvHandle); + } + return true; +} + +void Application::CleanupRenderTarget() { + for (UINT i = 0; i < 3; i++) { + if (m_renderTargets[i]) { + m_renderTargets[i]->Release(); + m_renderTargets[i] = nullptr; + } + } +} + +void Application::SetupDockspace() { + static ImGuiDockNodeFlags dockspaceFlags = ImGuiDockNodeFlags_NoWindowMenuButton; + + ImGuiWindowFlags windowFlags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking; + + ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + ImGui::SetNextWindowViewport(viewport->ID); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + windowFlags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + windowFlags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::Begin("MainDockspace", nullptr, windowFlags); + ImGui::PopStyleVar(); + ImGui::PopStyleVar(2); + + ImGuiID dockspaceId = ImGui::GetID("MyDockspace"); + ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), dockspaceFlags); + + static bool firstTime = true; + if (firstTime) { + firstTime = false; + ImGui::DockBuilderRemoveNode(dockspaceId); + ImGui::DockBuilderAddNode(dockspaceId, dockspaceFlags | ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(dockspaceId, viewport->Size); + + ImGuiID dockMain = dockspaceId; + ImGuiID dockBottom = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Down, 0.25f, nullptr, &dockMain); + ImGuiID dockLeft = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Left, 0.15f, nullptr, &dockMain); + ImGuiID dockRight = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Right, 0.25f, nullptr, &dockMain); + + ImGui::DockBuilderDockWindow("Hierarchy", dockLeft); + ImGui::DockBuilderDockWindow("Scene", dockMain); + ImGui::DockBuilderDockWindow("Game", dockMain); + ImGui::DockBuilderDockWindow("Inspector", dockRight); + ImGui::DockBuilderDockWindow("Console", dockBottom); + ImGui::DockBuilderDockWindow("Project", dockBottom); + + ImGui::DockBuilderFinish(dockspaceId); + } + + ImGui::End(); +} + +void Application::RenderUI() { + m_menuBar->Render(); + + m_hierarchyPanel->Render(); + m_sceneViewPanel->Render(); + m_gameViewPanel->Render(); + m_inspectorPanel->Render(); + m_consolePanel->Render(); + m_projectPanel->Render(); +} + +} \ No newline at end of file diff --git a/ui_editor/src/Application.h b/ui_editor/src/Application.h new file mode 100644 index 00000000..8e1d92dd --- /dev/null +++ b/ui_editor/src/Application.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include + +#include "Theme.h" +#include "panels/Panel.h" +#include "panels/MenuBar.h" +#include "panels/HierarchyPanel.h" +#include "panels/SceneViewPanel.h" +#include "panels/GameViewPanel.h" +#include "panels/InspectorPanel.h" +#include "panels/ConsolePanel.h" +#include "panels/ProjectPanel.h" + +namespace UI { + +class Application { +public: + static Application& Get(); + + bool Initialize(HWND hwnd); + void Shutdown(); + void Render(); + void OnResize(int width, int height); + +private: + Application() = default; + ~Application() = default; + + bool CreateDevice(); + bool CreateRenderTarget(); + void CleanupRenderTarget(); + void SetupDockspace(); + void RenderUI(); + + HWND m_hwnd = nullptr; + int m_width = 1280; + int m_height = 720; + + ID3D12Device* m_device = nullptr; + ID3D12CommandQueue* m_commandQueue = nullptr; + ID3D12CommandAllocator* m_commandAllocator = nullptr; + ID3D12GraphicsCommandList* m_commandList = nullptr; + IDXGISwapChain3* m_swapChain = nullptr; + ID3D12DescriptorHeap* m_rtvHeap = nullptr; + ID3D12DescriptorHeap* m_srvHeap = nullptr; + ID3D12Resource* m_renderTargets[3] = {}; + ID3D12Fence* m_fence = nullptr; + UINT64 m_fenceValue = 0; + UINT m_rtvDescriptorSize = 0; + UINT m_frameIndex = 0; + + std::unique_ptr m_menuBar; + std::unique_ptr m_hierarchyPanel; + std::unique_ptr m_sceneViewPanel; + std::unique_ptr m_gameViewPanel; + std::unique_ptr m_inspectorPanel; + std::unique_ptr m_consolePanel; + std::unique_ptr m_projectPanel; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/Core/AssetItem.h b/ui_editor/src/Core/AssetItem.h new file mode 100644 index 00000000..8875bcd3 --- /dev/null +++ b/ui_editor/src/Core/AssetItem.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +namespace UI { + +struct AssetItem { + std::string name; + std::string type; + bool isFolder; + std::string fullPath; + std::vector> children; +}; + +using AssetItemPtr = std::shared_ptr; + +} \ No newline at end of file diff --git a/ui_editor/src/Core/Event.h b/ui_editor/src/Core/Event.h new file mode 100644 index 00000000..2ab193b5 --- /dev/null +++ b/ui_editor/src/Core/Event.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +namespace UI { + +template +class Event { +public: + using HandlerID = size_t; + using Handler = std::function; + + HandlerID Subscribe(Handler handler) { + HandlerID id = m_nextId++; + m_handlers.emplace_back(id, std::move(handler)); + return id; + } + + void Unsubscribe(HandlerID id) { + m_handlers.erase( + std::remove_if(m_handlers.begin(), m_handlers.end(), + [id](const auto& pair) { return pair.first == id; }), + m_handlers.end() + ); + } + + void Invoke(Args... args) { + for (const auto& pair : m_handlers) { + pair.second(args...); + } + } + + void operator()(Args... args) { + Invoke(args...); + } + + void Clear() { + m_handlers.clear(); + } + +private: + HandlerID m_nextId = 0; + std::vector> m_handlers; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/Core/GameObject.h b/ui_editor/src/Core/GameObject.h new file mode 100644 index 00000000..f376a040 --- /dev/null +++ b/ui_editor/src/Core/GameObject.h @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace UI { + +using EntityID = uint64_t; +constexpr EntityID INVALID_ENTITY = 0; + +class Component { +public: + virtual ~Component() = default; + virtual std::string GetName() const = 0; +}; + +class TransformComponent : public Component { +public: + float position[3] = {0.0f, 0.0f, 0.0f}; + float rotation[3] = {0.0f, 0.0f, 0.0f}; + float scale[3] = {1.0f, 1.0f, 1.0f}; + + std::string GetName() const override { return "Transform"; } +}; + +class MeshRendererComponent : public Component { +public: + std::string materialName = "Default-Material"; + std::string meshName = ""; + + std::string GetName() const override { return "Mesh Renderer"; } +}; + +struct Entity { + EntityID id = INVALID_ENTITY; + std::string name; + EntityID parent = INVALID_ENTITY; + std::vector children; + std::vector> components; + bool selected = false; + + template + T* AddComponent(Args&&... args) { + auto comp = std::make_unique(std::forward(args)...); + T* ptr = comp.get(); + components.push_back(std::move(comp)); + return ptr; + } + + template + T* GetComponent() { + for (auto& comp : components) { + if (auto casted = dynamic_cast(comp.get())) { + return casted; + } + } + return nullptr; + } +}; + +using ComponentInspectorFn = std::function; + +struct ComponentInspectorInfo { + std::string name; + ComponentInspectorFn renderFn; +}; + +class ComponentRegistry { +public: + static ComponentRegistry& Get() { + static ComponentRegistry instance; + return instance; + } + + template + void RegisterComponent(const std::string& name, ComponentInspectorFn inspectorFn) { + m_inspectors[name] = {name, inspectorFn}; + m_factories[name] = []() -> std::unique_ptr { + return std::make_unique(); + }; + } + + ComponentInspectorInfo* GetInspector(const std::string& name) { + auto it = m_inspectors.find(name); + if (it != m_inspectors.end()) { + return &it->second; + } + return nullptr; + } + +private: + ComponentRegistry() = default; + std::unordered_map m_inspectors; + std::unordered_map()>> m_factories; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/Core/LogEntry.h b/ui_editor/src/Core/LogEntry.h new file mode 100644 index 00000000..e4218f09 --- /dev/null +++ b/ui_editor/src/Core/LogEntry.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace UI { + +struct LogEntry { + enum class Level { Info, Warning, Error }; + Level level; + std::string message; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/Managers/LogSystem.cpp b/ui_editor/src/Managers/LogSystem.cpp new file mode 100644 index 00000000..a0e06dcc --- /dev/null +++ b/ui_editor/src/Managers/LogSystem.cpp @@ -0,0 +1,19 @@ +#include "LogSystem.h" + +namespace UI { + +LogSystem& LogSystem::Get() { + static LogSystem instance; + return instance; +} + +void LogSystem::AddLog(LogEntry::Level level, const std::string& message) { + m_logs.push_back({level, message}); + if (m_callback) m_callback(); +} + +void LogSystem::Clear() { + m_logs.clear(); +} + +} \ No newline at end of file diff --git a/ui_editor/src/Managers/LogSystem.h b/ui_editor/src/Managers/LogSystem.h new file mode 100644 index 00000000..b480e907 --- /dev/null +++ b/ui_editor/src/Managers/LogSystem.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Core/LogEntry.h" +#include +#include + +namespace UI { + +class LogSystem { +public: + static LogSystem& Get(); + + void AddLog(LogEntry::Level level, const std::string& message); + void Clear(); + const std::vector& GetLogs() const { return m_logs; } + + void SetCallback(std::function callback) { m_callback = callback; } + +private: + LogSystem() = default; + + std::vector m_logs; + std::function m_callback; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/Managers/ProjectManager.cpp b/ui_editor/src/Managers/ProjectManager.cpp new file mode 100644 index 00000000..15d1d1e9 --- /dev/null +++ b/ui_editor/src/Managers/ProjectManager.cpp @@ -0,0 +1,246 @@ +#include "ProjectManager.h" +#include +#include +#include +#include + +namespace fs = std::filesystem; + +namespace UI { + +ProjectManager& ProjectManager::Get() { + static ProjectManager instance; + return instance; +} + +std::vector& ProjectManager::GetCurrentItems() { + if (m_path.empty()) { + static std::vector empty; + return empty; + } + return m_path.back()->children; +} + +void ProjectManager::NavigateToFolder(const AssetItemPtr& folder) { + m_path.push_back(folder); + m_selectedIndex = -1; +} + +void ProjectManager::NavigateBack() { + if (m_path.size() > 1) { + m_path.pop_back(); + m_selectedIndex = -1; + } +} + +void ProjectManager::NavigateToIndex(size_t index) { + if (index >= m_path.size()) return; + while (m_path.size() > index + 1) { + m_path.pop_back(); + } + m_selectedIndex = -1; +} + +std::string ProjectManager::GetCurrentPath() const { + if (m_path.empty()) return "Assets"; + std::string result = "Assets"; + for (size_t i = 1; i < m_path.size(); i++) { + result += "/"; + result += m_path[i]->name; + } + return result; +} + +std::string ProjectManager::GetPathName(size_t index) const { + if (index >= m_path.size()) return ""; + return m_path[index]->name; +} + +static std::wstring Utf8ToWstring(const std::string& str) { + if (str.empty()) return L""; + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, nullptr, 0); + if (len <= 0) return L""; + std::wstring result(len - 1, 0); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &result[0], len); + return result; +} + +static std::string WstringToUtf8(const std::wstring& wstr) { + if (wstr.empty()) return ""; + int len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (len <= 0) return ""; + std::string result(len - 1, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &result[0], len, nullptr, nullptr); + return result; +} + +void ProjectManager::Initialize(const std::string& projectPath) { + m_projectPath = projectPath; + + std::wstring projectPathW = Utf8ToWstring(projectPath); + fs::path assetsPath = fs::path(projectPathW) / L"Assets"; + + 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()); + std::ofstream((assetsPath / L"Scenes" / L"Main.unity").wstring()); + } + + m_rootFolder = ScanDirectory(assetsPath.wstring()); + m_rootFolder->name = "Assets"; + m_rootFolder->fullPath = WstringToUtf8(assetsPath.wstring()); + + m_path.clear(); + m_path.push_back(m_rootFolder); + m_selectedIndex = -1; + } catch (const std::exception& e) { + m_rootFolder = std::make_shared(); + m_rootFolder->name = "Assets"; + m_rootFolder->isFolder = true; + m_rootFolder->type = "Folder"; + m_path.push_back(m_rootFolder); + } +} + +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); + } + return fullPath; +} + +void ProjectManager::RefreshCurrentFolder() { + if (m_path.empty()) return; + + try { + auto newFolder = ScanDirectory(GetCurrentFullPathW()); + m_path.back()->children = newFolder->children; + } catch (...) { + } +} + +void ProjectManager::CreateFolder(const std::string& name) { + try { + std::wstring fullPath = GetCurrentFullPathW(); + fs::path newFolderPath = fs::path(fullPath) / Utf8ToWstring(name); + fs::create_directory(newFolderPath); + RefreshCurrentFolder(); + } catch (...) { + } +} + +void ProjectManager::DeleteItem(int index) { + if (m_path.empty()) return; + auto& items = m_path.back()->children; + if (index < 0 || index >= (int)items.size()) return; + + try { + std::wstring fullPath = GetCurrentFullPathW(); + fs::path itemPath = fs::path(fullPath) / Utf8ToWstring(items[index]->name); + fs::remove_all(itemPath); + m_selectedIndex = -1; + RefreshCurrentFolder(); + } catch (...) { + } +} + +bool ProjectManager::MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) { + try { + fs::path sourcePath = Utf8ToWstring(sourceFullPath); + fs::path destPath = fs::path(Utf8ToWstring(destFolderFullPath)) / sourcePath.filename(); + + if (!fs::exists(sourcePath)) { + return false; + } + + if (fs::exists(destPath)) { + return false; + } + + fs::rename(sourcePath, destPath); + RefreshCurrentFolder(); + return true; + } catch (...) { + return false; + } +} + +AssetItemPtr ProjectManager::ScanDirectory(const std::wstring& path) { + auto folder = std::make_shared(); + folder->name = WstringToUtf8(fs::path(path).filename().wstring()); + folder->isFolder = true; + folder->type = "Folder"; + + if (!fs::exists(path)) return folder; + + std::vector items; + + try { + for (const auto& entry : fs::directory_iterator(path)) { + std::wstring nameW = entry.path().filename().wstring(); + bool isFolder = entry.is_directory(); + items.push_back(CreateAssetItem(entry.path().wstring(), nameW, isFolder)); + } + } catch (...) { + } + + std::sort(items.begin(), items.end(), [](const AssetItemPtr& a, const AssetItemPtr& b) { + if (a->isFolder != b->isFolder) return a->isFolder; + return a->name < b->name; + }); + + folder->children = items; + return folder; +} + +AssetItemPtr ProjectManager::CreateAssetItem(const std::wstring& path, const std::wstring& nameW, bool isFolder) { + auto item = std::make_shared(); + item->name = WstringToUtf8(nameW); + item->isFolder = isFolder; + item->fullPath = WstringToUtf8(path); + + if (isFolder) { + item->type = "Folder"; + try { + auto subFolder = ScanDirectory(path); + item->children = subFolder->children; + } catch (...) { + } + } else { + std::wstring ext = fs::path(path).extension().wstring(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower); + + if (ext == L".png" || ext == L".jpg" || ext == L".tga" || ext == L".bmp") { + item->type = "Texture"; + } else if (ext == L".fbx" || ext == L".obj" || ext == L".gltf" || ext == L".glb") { + item->type = "Model"; + } else if (ext == L".cs" || ext == L".cpp" || ext == L".h") { + item->type = "Script"; + } else if (ext == L".mat") { + item->type = "Material"; + } else if (ext == L".unity" || ext == L".scene") { + item->type = "Scene"; + } else if (ext == L".prefab") { + item->type = "Prefab"; + } else { + item->type = "File"; + } + } + + return item; +} + +} \ No newline at end of file diff --git a/ui_editor/src/Managers/ProjectManager.h b/ui_editor/src/Managers/ProjectManager.h new file mode 100644 index 00000000..525aa0ea --- /dev/null +++ b/ui_editor/src/Managers/ProjectManager.h @@ -0,0 +1,49 @@ +#pragma once + +#include "Core/AssetItem.h" +#include +#include +#include + +namespace UI { + +class ProjectManager { +public: + static ProjectManager& Get(); + + std::vector& GetCurrentItems(); + int GetSelectedIndex() const { return m_selectedIndex; } + void SetSelectedIndex(int index) { m_selectedIndex = index; } + + void NavigateToFolder(const AssetItemPtr& folder); + void NavigateBack(); + void NavigateToIndex(size_t index); + bool CanNavigateBack() const { return m_path.size() > 1; } + + std::string GetCurrentPath() const; + size_t GetPathDepth() const { return m_path.size(); } + std::string GetPathName(size_t index) const; + + void Initialize(const std::string& projectPath); + void RefreshCurrentFolder(); + + void CreateFolder(const std::string& name); + void DeleteItem(int index); + bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath); + + const std::string& GetProjectPath() const { return m_projectPath; } + +private: + ProjectManager() = default; + + AssetItemPtr ScanDirectory(const std::wstring& path); + AssetItemPtr CreateAssetItem(const std::wstring& path, const std::wstring& nameW, bool isFolder); + std::wstring GetCurrentFullPathW() const; + + AssetItemPtr m_rootFolder; + std::vector m_path; + int m_selectedIndex = -1; + std::string m_projectPath; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/Managers/SceneManager.cpp b/ui_editor/src/Managers/SceneManager.cpp new file mode 100644 index 00000000..ba9d419d --- /dev/null +++ b/ui_editor/src/Managers/SceneManager.cpp @@ -0,0 +1,185 @@ +#include "SceneManager.h" +#include + +namespace UI { + +EntityID SceneManager::CreateEntity(const std::string& name, EntityID parent) { + EntityID id = m_nextEntityId++; + Entity entity; + entity.id = id; + entity.name = name; + entity.parent = parent; + m_entities[id] = std::move(entity); + + if (parent != INVALID_ENTITY) { + m_entities[parent].children.push_back(id); + } else { + m_rootEntities.push_back(id); + } + + OnEntityCreated.Invoke(id); + return id; +} + +void SceneManager::DeleteEntity(EntityID id) { + auto it = m_entities.find(id); + if (it == m_entities.end()) return; + + Entity& entity = it->second; + + std::vector childrenToDelete = entity.children; + for (EntityID childId : childrenToDelete) { + DeleteEntity(childId); + } + + if (entity.parent != INVALID_ENTITY) { + auto* parent = GetEntity(entity.parent); + if (parent) { + auto& siblings = parent->children; + siblings.erase(std::remove(siblings.begin(), siblings.end(), id), siblings.end()); + } + } else { + m_rootEntities.erase(std::remove(m_rootEntities.begin(), m_rootEntities.end(), id), m_rootEntities.end()); + } + + if (SelectionManager::Get().GetSelectedEntity() == id) { + SelectionManager::Get().ClearSelection(); + } + + m_entities.erase(it); + OnEntityDeleted.Invoke(id); +} + +ClipboardData SceneManager::CopyEntityRecursive(const Entity* entity) { + ClipboardData data; + data.name = entity->name; + + for (const auto& comp : entity->components) { + if (auto* transform = dynamic_cast(comp.get())) { + auto newComp = std::make_unique(); + memcpy(newComp->position, transform->position, sizeof(transform->position)); + memcpy(newComp->rotation, transform->rotation, sizeof(transform->rotation)); + memcpy(newComp->scale, transform->scale, sizeof(transform->scale)); + data.components.push_back(std::move(newComp)); + } + else if (auto* meshRenderer = dynamic_cast(comp.get())) { + auto newComp = std::make_unique(); + newComp->materialName = meshRenderer->materialName; + newComp->meshName = meshRenderer->meshName; + data.components.push_back(std::move(newComp)); + } + } + + for (EntityID childId : entity->children) { + const Entity* child = GetEntity(childId); + if (child) { + data.children.push_back(CopyEntityRecursive(child)); + } + } + + return data; +} + +void SceneManager::CopyEntity(EntityID id) { + const Entity* entity = GetEntity(id); + if (!entity) return; + + m_clipboard = CopyEntityRecursive(entity); +} + +EntityID SceneManager::PasteEntityRecursive(const ClipboardData& data, EntityID parent) { + EntityID newId = CreateEntity(data.name, parent); + Entity* newEntity = GetEntity(newId); + + if (newEntity) { + newEntity->components.clear(); + for (const auto& comp : data.components) { + if (auto* transform = dynamic_cast(comp.get())) { + auto newComp = std::make_unique(); + memcpy(newComp->position, transform->position, sizeof(transform->position)); + memcpy(newComp->rotation, transform->rotation, sizeof(transform->rotation)); + memcpy(newComp->scale, transform->scale, sizeof(transform->scale)); + newEntity->components.push_back(std::move(newComp)); + } + else if (auto* meshRenderer = dynamic_cast(comp.get())) { + auto newComp = std::make_unique(); + newComp->materialName = meshRenderer->materialName; + newComp->meshName = meshRenderer->meshName; + newEntity->components.push_back(std::move(newComp)); + } + } + } + + for (const auto& childData : data.children) { + PasteEntityRecursive(childData, newId); + } + + return newId; +} + +EntityID SceneManager::PasteEntity(EntityID parent) { + if (!m_clipboard) return INVALID_ENTITY; + return PasteEntityRecursive(*m_clipboard, parent); +} + +EntityID SceneManager::DuplicateEntity(EntityID id) { + CopyEntity(id); + const Entity* entity = GetEntity(id); + if (!entity) return INVALID_ENTITY; + return PasteEntity(entity->parent); +} + +void SceneManager::MoveEntity(EntityID id, EntityID newParent) { + Entity* entity = GetEntity(id); + if (!entity || id == newParent) return; + + if (entity->parent != INVALID_ENTITY) { + Entity* oldParent = GetEntity(entity->parent); + if (oldParent) { + auto& siblings = oldParent->children; + siblings.erase(std::remove(siblings.begin(), siblings.end(), id), siblings.end()); + } + } else { + m_rootEntities.erase(std::remove(m_rootEntities.begin(), m_rootEntities.end(), id), m_rootEntities.end()); + } + + entity->parent = newParent; + + if (newParent != INVALID_ENTITY) { + Entity* newParentEntity = GetEntity(newParent); + if (newParentEntity) { + newParentEntity->children.push_back(id); + } + } else { + m_rootEntities.push_back(id); + } + + OnEntityChanged.Invoke(id); +} + +void SceneManager::CreateDemoScene() { + m_entities.clear(); + m_rootEntities.clear(); + m_nextEntityId = 1; + m_clipboard.reset(); + + EntityID camera = CreateEntity("Main Camera"); + GetEntity(camera)->AddComponent(); + + EntityID light = CreateEntity("Directional Light"); + + EntityID cube = CreateEntity("Cube"); + GetEntity(cube)->AddComponent(); + GetEntity(cube)->AddComponent()->meshName = "Cube Mesh"; + + EntityID sphere = CreateEntity("Sphere"); + GetEntity(sphere)->AddComponent(); + GetEntity(sphere)->AddComponent()->meshName = "Sphere Mesh"; + + EntityID player = CreateEntity("Player"); + EntityID weapon = CreateEntity("Weapon", player); + + OnSceneChanged.Invoke(); +} + +} \ No newline at end of file diff --git a/ui_editor/src/Managers/SceneManager.h b/ui_editor/src/Managers/SceneManager.h new file mode 100644 index 00000000..49986e59 --- /dev/null +++ b/ui_editor/src/Managers/SceneManager.h @@ -0,0 +1,86 @@ +#pragma once + +#include "Core/GameObject.h" +#include "SelectionManager.h" +#include +#include +#include +#include + +namespace UI { + +struct ClipboardData { + std::string name; + std::vector> components; + std::vector children; +}; + +class SceneManager { +public: + static SceneManager& Get() { + static SceneManager instance; + return instance; + } + + EntityID CreateEntity(const std::string& name, EntityID parent = INVALID_ENTITY); + + Entity* GetEntity(EntityID id) { + auto it = m_entities.find(id); + if (it != m_entities.end()) { + return &it->second; + } + return nullptr; + } + + const Entity* GetEntity(EntityID id) const { + auto it = m_entities.find(id); + if (it != m_entities.end()) { + return &it->second; + } + return nullptr; + } + + const std::vector& GetRootEntities() const { + return m_rootEntities; + } + + void DeleteEntity(EntityID id); + + void RenameEntity(EntityID id, const std::string& newName) { + auto* entity = GetEntity(id); + if (entity) { + entity->name = newName; + OnEntityChanged.Invoke(id); + } + } + + void CopyEntity(EntityID id); + + EntityID PasteEntity(EntityID parent = INVALID_ENTITY); + + EntityID DuplicateEntity(EntityID id); + + void MoveEntity(EntityID id, EntityID newParent); + + void CreateDemoScene(); + + bool HasClipboardData() const { return m_clipboard.has_value(); } + + Event OnEntityCreated; + Event OnEntityDeleted; + Event OnEntityChanged; + Event<> OnSceneChanged; + +private: + SceneManager() = default; + + ClipboardData CopyEntityRecursive(const Entity* entity); + EntityID PasteEntityRecursive(const ClipboardData& data, EntityID parent); + + EntityID m_nextEntityId = 1; + std::unordered_map m_entities; + std::vector m_rootEntities; + std::optional m_clipboard; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/Managers/SelectionManager.h b/ui_editor/src/Managers/SelectionManager.h new file mode 100644 index 00000000..fe17e7b0 --- /dev/null +++ b/ui_editor/src/Managers/SelectionManager.h @@ -0,0 +1,38 @@ +#pragma once + +#include "Core/GameObject.h" +#include "Core/Event.h" +#include + +namespace UI { + +class SelectionManager { +public: + static SelectionManager& Get() { + static SelectionManager instance; + return instance; + } + + EntityID GetSelectedEntity() const { return m_selectedEntity; } + + void SetSelectedEntity(EntityID id) { + m_selectedEntity = id; + OnSelectionChanged.Invoke(id); + } + + void ClearSelection() { + SetSelectedEntity(INVALID_ENTITY); + } + + bool IsSelected(EntityID id) const { + return m_selectedEntity == id; + } + + Event OnSelectionChanged; + +private: + SelectionManager() = default; + EntityID m_selectedEntity = INVALID_ENTITY; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/Theme.cpp b/ui_editor/src/Theme.cpp new file mode 100644 index 00000000..6c5d7d93 --- /dev/null +++ b/ui_editor/src/Theme.cpp @@ -0,0 +1,82 @@ +#include "Theme.h" +#include + +namespace UI { + +void ApplyUnityDarkTheme() { + ImGuiStyle& style = ImGui::GetStyle(); + ImVec4* colors = style.Colors; + + colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.15f, 0.15f, 0.15f, 1.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.18f, 0.18f, 0.18f, 0.94f); + colors[ImGuiCol_Border] = ImVec4(0.08f, 0.08f, 0.08f, 1.00f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.28f, 0.28f, 0.28f, 1.00f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.35f, 0.35f, 0.35f, 1.00f); + colors[ImGuiCol_TitleBg] = ImVec4(0.15f, 0.15f, 0.15f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.12f, 0.12f, 0.12f, 0.75f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.10f, 0.10f, 0.10f, 0.53f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.40f, 0.40f, 0.40f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.80f, 0.80f, 0.80f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.35f, 0.35f, 0.35f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.45f, 0.45f, 0.45f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.35f, 0.35f, 0.35f, 1.00f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.45f, 0.45f, 0.45f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.08f, 0.08f, 0.08f, 1.00f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_Tab] = ImVec4(0.18f, 0.18f, 0.18f, 0.86f); + colors[ImGuiCol_TabHovered] = ImVec4(0.35f, 0.35f, 0.35f, 1.00f); + colors[ImGuiCol_TabActive] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); + colors[ImGuiCol_TabUnfocused] = ImVec4(0.15f, 0.15f, 0.15f, 0.97f); + colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.22f, 0.22f, 0.22f, 1.00f); + colors[ImGuiCol_DockingPreview] = ImVec4(0.40f, 0.40f, 0.40f, 0.70f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f); + colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.19f, 0.19f, 0.20f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.31f, 0.31f, 0.35f, 1.00f); + colors[ImGuiCol_TableBorderLight] = ImVec4(0.23f, 0.23f, 0.25f, 1.00f); + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.06f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.40f, 0.40f, 0.40f, 0.50f); + colors[ImGuiCol_DragDropTarget] = ImVec4(0.60f, 0.60f, 0.60f, 0.90f); + colors[ImGuiCol_NavHighlight] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); + + style.WindowRounding = 4.0f; + style.ChildRounding = 4.0f; + style.FrameRounding = 4.0f; + style.GrabRounding = 4.0f; + style.PopupRounding = 4.0f; + style.ScrollbarRounding = 4.0f; + style.TabRounding = 4.0f; + style.WindowBorderSize = 1.0f; + style.ChildBorderSize = 1.0f; + style.FrameBorderSize = 0.0f; + style.WindowPadding = ImVec2(8.0f, 8.0f); + style.FramePadding = ImVec2(6.0f, 4.0f); + style.ItemSpacing = ImVec2(8.0f, 4.0f); + style.ItemInnerSpacing = ImVec2(6.0f, 4.0f); +} + +} \ No newline at end of file diff --git a/ui_editor/src/Theme.h b/ui_editor/src/Theme.h new file mode 100644 index 00000000..6915ee9c --- /dev/null +++ b/ui_editor/src/Theme.h @@ -0,0 +1,7 @@ +#pragma once + +namespace UI { + +void ApplyUnityDarkTheme(); + +} \ No newline at end of file diff --git a/ui_editor/src/main.cpp b/ui_editor/src/main.cpp new file mode 100644 index 00000000..253643a2 --- /dev/null +++ b/ui_editor/src/main.cpp @@ -0,0 +1,96 @@ +#include "Application.h" +#include +#include +#include + +LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { + AllocConsole(); + freopen("CONOUT$", "w", stdout); + printf("Starting UI application...\n"); + + WNDCLASSEXW wc = {}; + wc.cbSize = sizeof(wc); + wc.style = CS_CLASSDC; + wc.lpfnWndProc = WndProc; + wc.hInstance = GetModuleHandle(nullptr); + wc.lpszClassName = L"XCVolumeRendererUI2"; + + if (!RegisterClassExW(&wc)) { + printf("Failed to register window class, error: %lu\n", GetLastError()); + return 1; + } + printf("Window class registered.\n"); + + HWND hwnd = CreateWindowExW( + 0, wc.lpszClassName, L"XCVolumeRenderer - Unity Style Editor", + WS_OVERLAPPEDWINDOW, 100, 100, 1280, 720, + nullptr, nullptr, wc.hInstance, nullptr + ); + + if (!hwnd) { + printf("Failed to create window, error: %lu\n", GetLastError()); + return 1; + } + printf("Window created.\n"); + + ShowWindow(hwnd, nCmdShow); + UpdateWindow(hwnd); + printf("Window shown.\n"); + + printf("Initializing application...\n"); + if (!UI::Application::Get().Initialize(hwnd)) { + printf("Failed to initialize application!\n"); + UnregisterClassW(wc.lpszClassName, wc.hInstance); + system("pause"); + return 1; + } + printf("Application initialized successfully.\n"); + + MSG msg = {}; + int frameCount = 0; + while (msg.message != WM_QUIT) { + if (PeekMessageW(&msg, nullptr, 0U, 0U, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } else { + UI::Application::Get().Render(); + frameCount++; + if (frameCount % 100 == 0) { + printf("Frame %d\n", frameCount); + } + } + } + + printf("Shutting down...\n"); + UI::Application::Get().Shutdown(); + UnregisterClassW(wc.lpszClassName, wc.hInstance); + + printf("Press any key to exit...\n"); + system("pause"); + return 0; +} + +extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { + if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) + return true; + + switch (msg) { + case WM_SIZE: + if (wParam != SIZE_MINIMIZED) { + UI::Application::Get().OnResize((int)LOWORD(lParam), (int)HIWORD(lParam)); + } + return 0; + case WM_SYSCOMMAND: + if ((wParam & 0xfff0) == SC_KEYMENU) + return 0; + break; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + } + return DefWindowProcW(hWnd, msg, wParam, lParam); +} \ No newline at end of file diff --git a/ui_editor/src/panels/ConsolePanel.cpp b/ui_editor/src/panels/ConsolePanel.cpp new file mode 100644 index 00000000..f7df0193 --- /dev/null +++ b/ui_editor/src/panels/ConsolePanel.cpp @@ -0,0 +1,70 @@ +#include "ConsolePanel.h" +#include "Managers/LogSystem.h" +#include "Core/LogEntry.h" +#include + +namespace UI { + +ConsolePanel::ConsolePanel() : Panel("Console") { + LogSystem::Get().AddLog(LogEntry::Level::Info, "Engine initialized successfully"); + LogSystem::Get().AddLog(LogEntry::Level::Info, "Loading default scene..."); + LogSystem::Get().AddLog(LogEntry::Level::Warning, "Missing material on object 'Cube'"); + LogSystem::Get().AddLog(LogEntry::Level::Error, "Failed to load texture: 'Assets/Textures/missing.png'"); + LogSystem::Get().AddLog(LogEntry::Level::Info, "Scene loaded successfully"); +} + +void ConsolePanel::Render() { + ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None); + + if (ImGui::Button("Clear")) { + LogSystem::Get().Clear(); + } + ImGui::SameLine(); + if (ImGui::Button("Info")) { + LogSystem::Get().AddLog(LogEntry::Level::Info, "Test info message"); + } + ImGui::SameLine(); + if (ImGui::Button("Warn")) { + LogSystem::Get().AddLog(LogEntry::Level::Warning, "Test warning message"); + } + ImGui::SameLine(); + if (ImGui::Button("Error")) { + LogSystem::Get().AddLog(LogEntry::Level::Error, "Test error message"); + } + + ImGui::Separator(); + + ImGui::BeginChild("LogScroll", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); + + for (const auto& log : LogSystem::Get().GetLogs()) { + ImVec4 color; + const char* prefix; + + switch (log.level) { + case LogEntry::Level::Info: + color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); + prefix = "[Info] "; + break; + case LogEntry::Level::Warning: + color = ImVec4(1.0f, 0.8f, 0.0f, 1.0f); + prefix = "[Warn] "; + break; + case LogEntry::Level::Error: + color = ImVec4(1.0f, 0.3f, 0.3f, 1.0f); + prefix = "[Error]"; + break; + } + + ImGui::TextColored(color, "%s%s", prefix, log.message.c_str()); + } + + if (m_scrollToBottom) { + ImGui::SetScrollHereY(1.0f); + m_scrollToBottom = false; + } + + ImGui::EndChild(); + ImGui::End(); +} + +} \ No newline at end of file diff --git a/ui_editor/src/panels/ConsolePanel.h b/ui_editor/src/panels/ConsolePanel.h new file mode 100644 index 00000000..4d73ef58 --- /dev/null +++ b/ui_editor/src/panels/ConsolePanel.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Panel.h" + +namespace UI { + +class ConsolePanel : public Panel { +public: + ConsolePanel(); + void Render() override; + +private: + bool m_scrollToBottom = false; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/panels/GameViewPanel.cpp b/ui_editor/src/panels/GameViewPanel.cpp new file mode 100644 index 00000000..d4389e1e --- /dev/null +++ b/ui_editor/src/panels/GameViewPanel.cpp @@ -0,0 +1,31 @@ +#include "GameViewPanel.h" +#include +#include + +namespace UI { + +GameViewPanel::GameViewPanel() : Panel("Game") {} + +void GameViewPanel::Render() { + ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None); + + RenderGameView(); + + ImGui::End(); +} + +void GameViewPanel::RenderGameView() { + ImVec2 canvasSize = ImGui::GetContentRegionAvail(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 canvasPos = ImGui::GetCursorScreenPos(); + + ImU32 bgColor = IM_COL32(20, 20, 25, 255); + drawList->AddRectFilled(canvasPos, ImVec2(canvasPos.x + canvasSize.x, canvasPos.y + canvasSize.y), bgColor); + + const char* text = "Game View (Press Play)"; + ImVec2 textSize = ImGui::CalcTextSize(text); + ImVec2 textPos(canvasPos.x + (canvasSize.x - textSize.x) * 0.5f, canvasPos.y + (canvasSize.y - textSize.y) * 0.5f); + drawList->AddText(textPos, IM_COL32(128, 128, 128, 255), text); +} + +} diff --git a/ui_editor/src/panels/GameViewPanel.h b/ui_editor/src/panels/GameViewPanel.h new file mode 100644 index 00000000..f0797cdf --- /dev/null +++ b/ui_editor/src/panels/GameViewPanel.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Panel.h" + +namespace UI { + +class GameViewPanel : public Panel { +public: + GameViewPanel(); + void Render() override; + +private: + void RenderGameView(); +}; + +} diff --git a/ui_editor/src/panels/HierarchyPanel.cpp b/ui_editor/src/panels/HierarchyPanel.cpp new file mode 100644 index 00000000..1be32dcc --- /dev/null +++ b/ui_editor/src/panels/HierarchyPanel.cpp @@ -0,0 +1,345 @@ +#include "HierarchyPanel.h" +#include "Managers/SceneManager.h" +#include "Managers/SelectionManager.h" +#include +#include + +namespace UI { + +HierarchyPanel::HierarchyPanel() : Panel("Hierarchy") { + SceneManager::Get().CreateDemoScene(); + + m_selectionHandlerId = SelectionManager::Get().OnSelectionChanged.Subscribe([this](EntityID) { + }); +} + +HierarchyPanel::~HierarchyPanel() { + SelectionManager::Get().OnSelectionChanged.Unsubscribe(m_selectionHandlerId); +} + +void HierarchyPanel::Render() { + ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None); + + RenderSearchBar(); + + ImGui::Separator(); + + HandleKeyboardShortcuts(); + + std::string filter = m_searchBuffer; + + ImGui::BeginChild("EntityList"); + + for (EntityID id : SceneManager::Get().GetRootEntities()) { + RenderEntity(id, filter); + } + + if (ImGui::IsWindowHovered() && ImGui::IsMouseDown(0) && !ImGui::IsAnyItemHovered()) { + if (!m_renaming) { + SelectionManager::Get().ClearSelection(); + } + } + + if (ImGui::BeginPopupContextWindow("HierarchyContextMenu", ImGuiPopupFlags_MouseButtonRight)) { + RenderCreateMenu(INVALID_ENTITY); + ImGui::EndPopup(); + } + + ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1)); + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_ID")) { + EntityID sourceId = *(const EntityID*)payload->Data; + if (sourceId != INVALID_ENTITY) { + const Entity* sourceEntity = SceneManager::Get().GetEntity(sourceId); + if (sourceEntity && sourceEntity->parent != INVALID_ENTITY) { + SceneManager::Get().MoveEntity(sourceId, INVALID_ENTITY); + } + } + } + ImGui::EndDragDropTarget(); + } + + ImGui::EndChild(); + + ImGui::End(); +} + +void HierarchyPanel::RenderSearchBar() { + ImGui::SetNextItemWidth(-1); + ImGui::InputTextWithHint("##Search", "Search...", m_searchBuffer, sizeof(m_searchBuffer)); +} + +void HierarchyPanel::RenderEntity(EntityID id, const std::string& filter) { + auto& sceneManager = SceneManager::Get(); + Entity* entity = sceneManager.GetEntity(id); + if (!entity) return; + + if (!filter.empty() && !PassesFilter(id, filter)) { + return; + } + + ImGui::PushID(static_cast(id)); + + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth; + + if (entity->children.empty()) { + flags |= ImGuiTreeNodeFlags_Leaf; + } + + if (SelectionManager::Get().IsSelected(id)) { + flags |= ImGuiTreeNodeFlags_Selected; + } + + if (m_renaming && m_renamingEntity == id) { + if (m_renameJustStarted) { + ImGui::SetKeyboardFocusHere(); + m_renameJustStarted = false; + } + + ImGui::SetNextItemWidth(-1); + if (ImGui::InputText("##Rename", m_renameBuffer, sizeof(m_renameBuffer), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) { + if (strlen(m_renameBuffer) > 0) { + sceneManager.RenameEntity(id, m_renameBuffer); + } + m_renaming = false; + m_renamingEntity = INVALID_ENTITY; + } + + if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) { + if (strlen(m_renameBuffer) > 0) { + sceneManager.RenameEntity(id, m_renameBuffer); + } + m_renaming = false; + m_renamingEntity = INVALID_ENTITY; + } + } else { + bool isOpen = ImGui::TreeNodeEx(entity->name.c_str(), flags); + + if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) { + SelectionManager::Get().SetSelectedEntity(id); + } + + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { + m_renaming = true; + m_renamingEntity = id; + strcpy_s(m_renameBuffer, entity->name.c_str()); + m_renameJustStarted = true; + } + + HandleDragDrop(id); + + if (ImGui::BeginPopupContextItem("EntityContextMenu")) { + RenderContextMenu(id); + ImGui::EndPopup(); + } + + if (isOpen) { + for (EntityID childId : entity->children) { + RenderEntity(childId, filter); + } + ImGui::TreePop(); + } + } + + ImGui::PopID(); +} + +void HierarchyPanel::RenderContextMenu(EntityID id) { + auto& sceneManager = SceneManager::Get(); + auto& selectionManager = SelectionManager::Get(); + + if (ImGui::BeginMenu("Create")) { + RenderCreateMenu(id); + ImGui::EndMenu(); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Rename", "F2")) { + const Entity* entity = sceneManager.GetEntity(id); + if (entity) { + m_renaming = true; + m_renamingEntity = id; + strcpy_s(m_renameBuffer, entity->name.c_str()); + m_renameJustStarted = true; + } + } + + if (ImGui::MenuItem("Delete", "Delete")) { + sceneManager.DeleteEntity(id); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Copy", "Ctrl+C")) { + sceneManager.CopyEntity(id); + } + + if (ImGui::MenuItem("Paste", "Ctrl+V", false, sceneManager.HasClipboardData())) { + sceneManager.PasteEntity(id); + } + + if (ImGui::MenuItem("Duplicate", "Ctrl+D")) { + EntityID newId = sceneManager.DuplicateEntity(id); + if (newId != INVALID_ENTITY) { + selectionManager.SetSelectedEntity(newId); + } + } +} + +void HierarchyPanel::RenderCreateMenu(EntityID parent) { + auto& sceneManager = SceneManager::Get(); + auto& selectionManager = SelectionManager::Get(); + + if (ImGui::MenuItem("Empty Object")) { + EntityID newId = sceneManager.CreateEntity("GameObject", parent); + selectionManager.SetSelectedEntity(newId); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Camera")) { + EntityID newId = sceneManager.CreateEntity("Camera", parent); + sceneManager.GetEntity(newId)->AddComponent(); + selectionManager.SetSelectedEntity(newId); + } + + if (ImGui::MenuItem("Light")) { + EntityID newId = sceneManager.CreateEntity("Light", parent); + selectionManager.SetSelectedEntity(newId); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Cube")) { + EntityID newId = sceneManager.CreateEntity("Cube", parent); + sceneManager.GetEntity(newId)->AddComponent(); + sceneManager.GetEntity(newId)->AddComponent()->meshName = "Cube"; + selectionManager.SetSelectedEntity(newId); + } + + if (ImGui::MenuItem("Sphere")) { + EntityID newId = sceneManager.CreateEntity("Sphere", parent); + sceneManager.GetEntity(newId)->AddComponent(); + sceneManager.GetEntity(newId)->AddComponent()->meshName = "Sphere"; + selectionManager.SetSelectedEntity(newId); + } + + if (ImGui::MenuItem("Plane")) { + EntityID newId = sceneManager.CreateEntity("Plane", parent); + sceneManager.GetEntity(newId)->AddComponent(); + sceneManager.GetEntity(newId)->AddComponent()->meshName = "Plane"; + selectionManager.SetSelectedEntity(newId); + } +} + +void HierarchyPanel::HandleDragDrop(EntityID id) { + auto& sceneManager = SceneManager::Get(); + + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { + m_dragSource = id; + ImGui::SetDragDropPayload("ENTITY_ID", &id, sizeof(EntityID)); + const Entity* entity = sceneManager.GetEntity(id); + if (entity) { + ImGui::Text("%s", entity->name.c_str()); + } + ImGui::EndDragDropSource(); + } + + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_ID")) { + EntityID sourceId = *(const EntityID*)payload->Data; + if (sourceId != id && sourceId != INVALID_ENTITY) { + const Entity* targetEntity = sceneManager.GetEntity(id); + const Entity* sourceEntity = sceneManager.GetEntity(sourceId); + + bool isValidMove = true; + EntityID checkParent = targetEntity ? targetEntity->parent : INVALID_ENTITY; + while (checkParent != INVALID_ENTITY) { + if (checkParent == sourceId) { + isValidMove = false; + break; + } + const Entity* parentEntity = sceneManager.GetEntity(checkParent); + checkParent = parentEntity ? parentEntity->parent : INVALID_ENTITY; + } + + if (isValidMove && sourceEntity && sourceEntity->parent != id) { + sceneManager.MoveEntity(sourceId, id); + } + } + } + ImGui::EndDragDropTarget(); + } +} + +void HierarchyPanel::HandleKeyboardShortcuts() { + auto& sceneManager = SceneManager::Get(); + auto& selectionManager = SelectionManager::Get(); + + EntityID selectedId = selectionManager.GetSelectedEntity(); + + if (ImGui::IsWindowFocused()) { + if (ImGui::IsKeyPressed(ImGuiKey_Delete)) { + if (selectedId != INVALID_ENTITY) { + sceneManager.DeleteEntity(selectedId); + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_F2)) { + if (selectedId != INVALID_ENTITY) { + const Entity* entity = sceneManager.GetEntity(selectedId); + if (entity) { + m_renaming = true; + m_renamingEntity = selectedId; + strcpy_s(m_renameBuffer, entity->name.c_str()); + m_renameJustStarted = true; + } + } + } + + ImGuiIO& io = ImGui::GetIO(); + if (io.KeyCtrl) { + if (ImGui::IsKeyPressed(ImGuiKey_C)) { + if (selectedId != INVALID_ENTITY) { + sceneManager.CopyEntity(selectedId); + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_V)) { + if (sceneManager.HasClipboardData()) { + sceneManager.PasteEntity(selectedId); + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_D)) { + if (selectedId != INVALID_ENTITY) { + EntityID newId = sceneManager.DuplicateEntity(selectedId); + if (newId != INVALID_ENTITY) { + selectionManager.SetSelectedEntity(newId); + } + } + } + } + } +} + +bool HierarchyPanel::PassesFilter(EntityID id, const std::string& filter) { + auto& sceneManager = SceneManager::Get(); + const Entity* entity = sceneManager.GetEntity(id); + if (!entity) return false; + + if (entity->name.find(filter) != std::string::npos) { + return true; + } + + for (EntityID childId : entity->children) { + if (PassesFilter(childId, filter)) { + return true; + } + } + + return false; +} + +} \ No newline at end of file diff --git a/ui_editor/src/panels/HierarchyPanel.h b/ui_editor/src/panels/HierarchyPanel.h new file mode 100644 index 00000000..2b1ab3ed --- /dev/null +++ b/ui_editor/src/panels/HierarchyPanel.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Panel.h" +#include "Core/Event.h" +#include "Core/GameObject.h" + +namespace UI { + +class HierarchyPanel : public Panel { +public: + HierarchyPanel(); + ~HierarchyPanel(); + + void Render() override; + +private: + void RenderSearchBar(); + void RenderEntity(EntityID id, const std::string& filter); + void RenderContextMenu(EntityID id); + void RenderCreateMenu(EntityID parent); + void HandleDragDrop(EntityID id); + void HandleKeyboardShortcuts(); + bool PassesFilter(EntityID id, const std::string& filter); + + Event::HandlerID m_selectionHandlerId = 0; + + char m_searchBuffer[256] = ""; + bool m_renaming = false; + EntityID m_renamingEntity = INVALID_ENTITY; + char m_renameBuffer[256] = ""; + bool m_renameJustStarted = false; + EntityID m_dragSource = INVALID_ENTITY; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/panels/InspectorPanel.cpp b/ui_editor/src/panels/InspectorPanel.cpp new file mode 100644 index 00000000..b3899edf --- /dev/null +++ b/ui_editor/src/panels/InspectorPanel.cpp @@ -0,0 +1,94 @@ +#include "InspectorPanel.h" +#include "Managers/SceneManager.h" +#include "Managers/SelectionManager.h" +#include +#include + +namespace UI { + +InspectorPanel::InspectorPanel() : Panel("Inspector") { + m_selectionHandlerId = SelectionManager::Get().OnSelectionChanged.Subscribe([this](EntityID) { + }); +} + +InspectorPanel::~InspectorPanel() { + SelectionManager::Get().OnSelectionChanged.Unsubscribe(m_selectionHandlerId); +} + +void InspectorPanel::Render() { + ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None); + + EntityID selectedId = SelectionManager::Get().GetSelectedEntity(); + Entity* entity = SceneManager::Get().GetEntity(selectedId); + + if (entity) { + RenderEntity(entity); + } else { + ImGui::Text("No object selected"); + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Select an object in Hierarchy"); + } + + ImGui::End(); +} + +void InspectorPanel::RenderEntity(Entity* entity) { + ImGui::Text("%s", entity->name.c_str()); + ImGui::Separator(); + + for (auto& component : entity->components) { + RenderComponent(component.get()); + ImGui::Separator(); + } +} + +void InspectorPanel::RenderComponent(Component* component) { + if (!component) return; + + const char* name = component->GetName().c_str(); + + std::string headerId = name + std::string("##") + std::to_string(reinterpret_cast(component)); + + if (ImGui::CollapsingHeader(headerId.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(10.0f); + + if (auto* transform = dynamic_cast(component)) { + ImGui::Text("Position"); + ImGui::SameLine(80); + ImGui::SetNextItemWidth(180); + ImGui::DragFloat3("##Position", transform->position, 0.1f); + + ImGui::Text("Rotation"); + ImGui::SameLine(80); + ImGui::SetNextItemWidth(180); + ImGui::DragFloat3("##Rotation", transform->rotation, 1.0f); + + ImGui::Text("Scale"); + ImGui::SameLine(80); + ImGui::SetNextItemWidth(180); + ImGui::DragFloat3("##Scale", transform->scale, 0.1f); + } + else if (auto* meshRenderer = dynamic_cast(component)) { + char materialBuffer[256] = {}; + strncpy_s(materialBuffer, meshRenderer->materialName.c_str(), sizeof(materialBuffer) - 1); + ImGui::Text("Material"); + ImGui::SameLine(80); + ImGui::SetNextItemWidth(180); + if (ImGui::InputText("##Material", materialBuffer, sizeof(materialBuffer))) { + meshRenderer->materialName = materialBuffer; + } + + char meshBuffer[256] = {}; + strncpy_s(meshBuffer, meshRenderer->meshName.c_str(), sizeof(meshBuffer) - 1); + ImGui::Text("Mesh"); + ImGui::SameLine(80); + ImGui::SetNextItemWidth(180); + if (ImGui::InputText("##Mesh", meshBuffer, sizeof(meshBuffer))) { + meshRenderer->meshName = meshBuffer; + } + } + + ImGui::Unindent(10.0f); + } +} + +} \ No newline at end of file diff --git a/ui_editor/src/panels/InspectorPanel.h b/ui_editor/src/panels/InspectorPanel.h new file mode 100644 index 00000000..74945608 --- /dev/null +++ b/ui_editor/src/panels/InspectorPanel.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Panel.h" +#include "Core/Event.h" +#include "Core/GameObject.h" + +namespace UI { + +class InspectorPanel : public Panel { +public: + InspectorPanel(); + ~InspectorPanel(); + + void Render() override; + +private: + void RenderEntity(Entity* entity); + void RenderComponent(Component* component); + + Event::HandlerID m_selectionHandlerId = 0; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/panels/MenuBar.cpp b/ui_editor/src/panels/MenuBar.cpp new file mode 100644 index 00000000..9376a06f --- /dev/null +++ b/ui_editor/src/panels/MenuBar.cpp @@ -0,0 +1,55 @@ +#include "MenuBar.h" +#include + +namespace UI { + +MenuBar::MenuBar() : Panel("MenuBar") {} + +void MenuBar::Render() { + if (ImGui::BeginMainMenuBar()) { + ShowFileMenu(); + ShowEditMenu(); + ShowViewMenu(); + ShowHelpMenu(); + ImGui::EndMainMenuBar(); + } +} + +void MenuBar::ShowFileMenu() { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("New Scene", "Ctrl+N")) {} + if (ImGui::MenuItem("Open Scene", "Ctrl+O")) {} + if (ImGui::MenuItem("Save Scene", "Ctrl+S")) {} + ImGui::Separator(); + if (ImGui::MenuItem("Exit", "Alt+F4")) {} + ImGui::EndMenu(); + } +} + +void MenuBar::ShowEditMenu() { + if (ImGui::BeginMenu("Edit")) { + if (ImGui::MenuItem("Undo", "Ctrl+Z")) {} + if (ImGui::MenuItem("Redo", "Ctrl+Y")) {} + ImGui::Separator(); + if (ImGui::MenuItem("Cut", "Ctrl+X")) {} + if (ImGui::MenuItem("Copy", "Ctrl+C")) {} + if (ImGui::MenuItem("Paste", "Ctrl+V")) {} + ImGui::EndMenu(); + } +} + +void MenuBar::ShowViewMenu() { + if (ImGui::BeginMenu("View")) { + if (ImGui::MenuItem("Reset Layout")) {} + ImGui::EndMenu(); + } +} + +void MenuBar::ShowHelpMenu() { + if (ImGui::BeginMenu("Help")) { + if (ImGui::MenuItem("About")) {} + ImGui::EndMenu(); + } +} + +} \ No newline at end of file diff --git a/ui_editor/src/panels/MenuBar.h b/ui_editor/src/panels/MenuBar.h new file mode 100644 index 00000000..b4481422 --- /dev/null +++ b/ui_editor/src/panels/MenuBar.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Panel.h" + +namespace UI { + +class MenuBar : public Panel { +public: + MenuBar(); + void Render() override; + +private: + void ShowFileMenu(); + void ShowEditMenu(); + void ShowViewMenu(); + void ShowHelpMenu(); +}; + +} \ No newline at end of file diff --git a/ui_editor/src/panels/Panel.cpp b/ui_editor/src/panels/Panel.cpp new file mode 100644 index 00000000..bd3d0c3b --- /dev/null +++ b/ui_editor/src/panels/Panel.cpp @@ -0,0 +1,4 @@ +#include "Panel.h" + +namespace UI { +} \ No newline at end of file diff --git a/ui_editor/src/panels/Panel.h b/ui_editor/src/panels/Panel.h new file mode 100644 index 00000000..81c4b501 --- /dev/null +++ b/ui_editor/src/panels/Panel.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace UI { + +class Panel { +public: + Panel(const std::string& name) : m_name(name), m_isOpen(true) {} + virtual ~Panel() = default; + + virtual void Render() = 0; + + const std::string& GetName() const { return m_name; } + bool IsOpen() const { return m_isOpen; } + void SetOpen(bool open) { m_isOpen = open; } + void Toggle() { m_isOpen = !m_isOpen; } + +protected: + std::string m_name; + bool m_isOpen; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/panels/ProjectPanel.cpp b/ui_editor/src/panels/ProjectPanel.cpp new file mode 100644 index 00000000..a3b2e6e9 --- /dev/null +++ b/ui_editor/src/panels/ProjectPanel.cpp @@ -0,0 +1,297 @@ +#include "ProjectPanel.h" +#include "Managers/ProjectManager.h" +#include "Core/AssetItem.h" +#include +#include + +namespace UI { + +const char* DRAG_DROP_TYPE = "ASSET_ITEM"; + +ProjectPanel::ProjectPanel() : Panel("Project") { +} + +void ProjectPanel::Initialize(const std::string& projectPath) { + ProjectManager::Get().Initialize(projectPath); +} + +void ProjectPanel::Render() { + const ImGuiPayload* payload = ImGui::GetDragDropPayload(); + if (payload && payload->IsDataType(DRAG_DROP_TYPE)) { + m_draggingPath = (const char*)payload->Data; + } else if (!ImGui::IsMouseDown(0)) { + m_draggingPath.clear(); + } + + ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None); + + auto& manager = ProjectManager::Get(); + + bool canGoBack = manager.CanNavigateBack(); + ImGui::BeginDisabled(!canGoBack); + if (ImGui::Button("<")) { + if (canGoBack) { + manager.NavigateBack(); + } + } + ImGui::EndDisabled(); + ImGui::SameLine(); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0)); + size_t pathDepth = manager.GetPathDepth(); + for (size_t i = 0; i < pathDepth; i++) { + if (i > 0) { + ImGui::SameLine(); + ImGui::Text("/"); + ImGui::SameLine(); + } + std::string name = manager.GetPathName(i); + if (i < pathDepth - 1) { + if (ImGui::Button(name.c_str())) { + manager.NavigateToIndex(i); + } + } else { + ImGui::Text("%s", name.c_str()); + } + } + ImGui::PopStyleColor(2); + + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 80.0f); + if (ImGui::Button("Refresh")) { + manager.RefreshCurrentFolder(); + } + + ImGui::Separator(); + + ImGui::PushItemWidth(-1); + ImGui::InputTextWithHint("##Search", "Search...", m_searchBuffer, sizeof(m_searchBuffer)); + ImGui::PopItemWidth(); + + ImGui::Separator(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10, 10)); + + float buttonWidth = 80.0f; + float padding = 10.0f; + float panelWidth = ImGui::GetContentRegionAvail().x; + int columns = (int)(panelWidth / (buttonWidth + padding)); + if (columns < 1) columns = 1; + + auto& items = manager.GetCurrentItems(); + std::string searchStr = m_searchBuffer; + int itemIndex = 0; + + for (int i = 0; i < (int)items.size(); i++) { + if (!searchStr.empty()) { + if (items[i]->name.find(searchStr) == std::string::npos) { + continue; + } + } + + if (itemIndex > 0 && itemIndex % columns != 0) { + ImGui::SameLine(); + } + RenderAssetItem(items[i], itemIndex); + itemIndex++; + } + + ImGui::PopStyleVar(); + + if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered()) { + manager.SetSelectedIndex(-1); + } + + if (ImGui::BeginPopup("ItemContextMenu")) { + if (m_contextMenuIndex >= 0 && m_contextMenuIndex < (int)items.size()) { + auto& item = items[m_contextMenuIndex]; + if (item->isFolder) { + if (ImGui::MenuItem("Open")) { + manager.NavigateToFolder(item); + } + ImGui::Separator(); + } + if (ImGui::MenuItem("Delete")) { + manager.DeleteItem(m_contextMenuIndex); + m_contextMenuIndex = -1; + } + } + ImGui::EndPopup(); + } + + if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(1) && !ImGui::IsAnyItemHovered()) { + ImGui::OpenPopup("EmptyContextMenu"); + } + + if (ImGui::BeginPopup("EmptyContextMenu")) { + if (ImGui::MenuItem("Create Folder")) { + m_showCreateFolderPopup = true; + strcpy_s(m_newFolderName, "NewFolder"); + } + ImGui::Separator(); + if (ImGui::MenuItem("Refresh")) { + manager.RefreshCurrentFolder(); + } + ImGui::EndPopup(); + } + + ImGui::End(); + + if (m_showCreateFolderPopup) { + ImGui::OpenPopup("Create Folder"); + m_showCreateFolderPopup = false; + } + + if (ImGui::BeginPopupModal("Create Folder", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::InputText("Name", m_newFolderName, sizeof(m_newFolderName)); + ImGui::Separator(); + if (ImGui::Button("Create", ImVec2(80, 0))) { + CreateNewFolder(m_newFolderName); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(80, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) { + auto& manager = ProjectManager::Get(); + bool isSelected = (manager.GetSelectedIndex() == index); + + ImGui::PushID(index); + + if (isSelected) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.40f, 0.40f, 0.40f, 0.50f)); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.30f, 0.30f, 0.30f, 0.40f)); + } + + ImVec2 buttonSize(80.0f, 90.0f); + + if (ImGui::Button("##AssetBtn", buttonSize)) { + manager.SetSelectedIndex(index); + } + + bool doubleClicked = false; + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { + doubleClicked = true; + } + + bool openContextMenu = false; + if (ImGui::IsItemClicked(1)) { + manager.SetSelectedIndex(index); + m_contextMenuIndex = index; + openContextMenu = true; + } + + if (isSelected) { + ImGui::PopStyleColor(); + } else { + ImGui::PopStyleColor(2); + } + + ImVec2 min = ImGui::GetItemRectMin(); + ImVec2 max = ImVec2(min.x + buttonSize.x, min.y + buttonSize.y); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + if (!m_draggingPath.empty() && item->fullPath == m_draggingPath) { + drawList->AddRectFilled(min, max, IM_COL32(0, 0, 0, 60), 0.0f); + } + + ImU32 iconColor; + if (item->isFolder) { + iconColor = IM_COL32(200, 180, 100, 255); + } else if (item->type == "Texture") { + iconColor = IM_COL32(150, 200, 150, 255); + } else if (item->type == "Model") { + iconColor = IM_COL32(150, 150, 200, 255); + } else if (item->type == "Script") { + iconColor = IM_COL32(200, 150, 150, 255); + } else if (item->type == "Scene") { + iconColor = IM_COL32(200, 200, 150, 255); + } else { + iconColor = IM_COL32(100, 150, 200, 255); + } + + float iconSize = 40.0f; + ImVec2 iconMin(min.x + (80.0f - iconSize) * 0.5f, min.y + 10.0f); + ImVec2 iconMax(iconMin.x + iconSize, iconMin.y + iconSize); + drawList->AddRectFilled(iconMin, iconMax, iconColor, 4.0f); + + ImVec4 textColor = isSelected ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0.8f, 0.8f, 0.8f, 1.0f); + ImVec2 textSize = ImGui::CalcTextSize(item->name.c_str()); + float textOffset = std::max(0.0f, (80.0f - textSize.x) * 0.5f); + + ImGui::PushClipRect(min, ImVec2(min.x + 80.0f, min.y + 90.0f), true); + drawList->AddText(ImVec2(min.x + textOffset, min.y + 60.0f), ImGui::GetColorU32(textColor), item->name.c_str()); + ImGui::PopClipRect(); + + if (item->isFolder) { + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(DRAG_DROP_TYPE)) { + const char* draggedPath = (const char*)payload->Data; + std::string sourcePath(draggedPath); + manager.MoveItem(sourcePath, item->fullPath); + } + ImGui::EndDragDropTarget(); + } + + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) { + ImDrawList* hoverDrawList = ImGui::GetWindowDrawList(); + hoverDrawList->AddRect(min, ImVec2(min.x + buttonSize.x, min.y + buttonSize.y), IM_COL32(255, 255, 255, 80), 4.0f); + } + } + + if (!item->fullPath.empty()) { + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { + ImGui::SetDragDropPayload(DRAG_DROP_TYPE, item->fullPath.c_str(), item->fullPath.length() + 1); + + ImU32 iconColor; + if (item->isFolder) { + iconColor = IM_COL32(200, 180, 100, 100); + } else if (item->type == "Texture") { + iconColor = IM_COL32(150, 200, 150, 100); + } else if (item->type == "Model") { + iconColor = IM_COL32(150, 150, 200, 100); + } else if (item->type == "Script") { + iconColor = IM_COL32(200, 150, 150, 100); + } else if (item->type == "Scene") { + iconColor = IM_COL32(200, 200, 150, 100); + } else { + iconColor = IM_COL32(100, 150, 200, 100); + } + + ImVec2 previewMin = ImGui::GetMousePos(); + ImVec2 previewMax = ImVec2(previewMin.x + 40, previewMin.y + 40); + ImGui::GetForegroundDrawList()->AddRectFilled(previewMin, previewMax, iconColor, 4.0f); + + ImGui::EndDragDropSource(); + } + } + + if (doubleClicked && item->isFolder) { + manager.NavigateToFolder(item); + } + + ImGui::PopID(); + + if (openContextMenu) { + ImGui::OpenPopup("ItemContextMenu"); + } +} + +void ProjectPanel::CreateNewFolder(const std::string& name) { + auto& manager = ProjectManager::Get(); + manager.CreateFolder(name); +} + +bool ProjectPanel::HandleDrop(const AssetItemPtr& targetFolder) { + return false; +} + +} \ No newline at end of file diff --git a/ui_editor/src/panels/ProjectPanel.h b/ui_editor/src/panels/ProjectPanel.h new file mode 100644 index 00000000..5aea84d7 --- /dev/null +++ b/ui_editor/src/panels/ProjectPanel.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Panel.h" +#include "Core/AssetItem.h" + +namespace UI { + +class ProjectPanel : public Panel { +public: + ProjectPanel(); + void Render() override; + void Initialize(const std::string& projectPath); + +private: + void RenderAssetItem(const AssetItemPtr& item, int index); + void CreateNewFolder(const std::string& name); + bool HandleDrop(const AssetItemPtr& targetFolder); + + char m_searchBuffer[256] = ""; + bool m_showCreateFolderPopup = false; + char m_newFolderName[256] = "NewFolder"; + int m_contextMenuIndex = -1; + std::string m_draggingPath; +}; + +} \ No newline at end of file diff --git a/ui_editor/src/panels/SceneViewPanel.cpp b/ui_editor/src/panels/SceneViewPanel.cpp new file mode 100644 index 00000000..6fc8d98d --- /dev/null +++ b/ui_editor/src/panels/SceneViewPanel.cpp @@ -0,0 +1,54 @@ +#include "SceneViewPanel.h" +#include +#include + +namespace UI { + +SceneViewPanel::SceneViewPanel() : Panel("Scene") {} + +void SceneViewPanel::Render() { + ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None); + + ImVec2 canvasSize = ImGui::GetContentRegionAvail(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 canvasPos = ImGui::GetCursorScreenPos(); + + ImU32 bgColor = IM_COL32(30, 30, 30, 255); + drawList->AddRectFilled(canvasPos, ImVec2(canvasPos.x + canvasSize.x, canvasPos.y + canvasSize.y), bgColor); + + RenderGrid(); + + ImGui::End(); +} + +void SceneViewPanel::RenderGrid() { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 canvasPos = ImGui::GetCursorScreenPos(); + ImVec2 canvasSize = ImGui::GetContentRegionAvail(); + + float gridSize = 50.0f; + ImU32 gridColor = IM_COL32(50, 50, 50, 255); + + for (float x = fmodf(0, gridSize); x < canvasSize.x; x += gridSize) { + drawList->AddLine( + ImVec2(canvasPos.x + x, canvasPos.y), + ImVec2(canvasPos.x + x, canvasPos.y + canvasSize.y), + gridColor + ); + } + + for (float y = fmodf(0, gridSize); y < canvasSize.y; y += gridSize) { + drawList->AddLine( + ImVec2(canvasPos.x, canvasPos.y + y), + ImVec2(canvasPos.x + canvasSize.x, canvasPos.y + y), + gridColor + ); + } + + const char* label = "Scene View"; + ImVec2 labelSize = ImGui::CalcTextSize(label); + ImVec2 labelPos(canvasPos.x + 10, canvasPos.y + 10); + drawList->AddText(labelPos, IM_COL32(100, 100, 100, 255), label); +} + +} \ No newline at end of file diff --git a/ui_editor/src/panels/SceneViewPanel.h b/ui_editor/src/panels/SceneViewPanel.h new file mode 100644 index 00000000..d81c47ec --- /dev/null +++ b/ui_editor/src/panels/SceneViewPanel.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Panel.h" + +namespace UI { + +class SceneViewPanel : public Panel { +public: + SceneViewPanel(); + void Render() override; + +private: + void RenderGrid(); +}; + +} \ No newline at end of file