diff --git a/docs/plan/Editor重构3.26.md b/docs/plan/Editor重构3.26.md new file mode 100644 index 00000000..ff9b0d72 --- /dev/null +++ b/docs/plan/Editor重构3.26.md @@ -0,0 +1,271 @@ +# Editor重构 3.26 + +## 当前判断 + +截至 2026-03-26,Editor 的 UI 架构重构如果只计算“架构层整理”,不计算 Viewport/RHI 和 Scene/Game 真正内容开发,整体进度大约在 **90%**。 + +如果把“编辑器整体可用度”也算进去,则大约在 **65%** 左右,因为 `Scene` / `Game` 面板目前仍然只是占位壳,Viewport 仍未回归,且部分编辑语义与回归验证还未补齐。 + +## 已完成的架构层 + +### 1. UI 主题与样式 Token 层 + +已将大量直接散落在 panel 中的颜色、间距、控件尺寸、Dock 外观收口到统一 UI 基础层。 + +当前主要入口: + +- `editor/src/UI/BaseTheme.h` +- `editor/src/UI/StyleTokens.h` +- `editor/src/UI/DockHostStyle.h` +- `editor/src/UI/PanelChrome.h` +- `editor/src/UI/Core.h` +- `editor/src/UI/Widgets.h` +- `editor/src/UI/PopupState.h` +- `editor/src/UI/PropertyGrid.h` +- `editor/src/UI/UI.h` + +这一层的意义是: + +- 主题值不再零散写在各个 panel 里 +- panel chrome、toolbar、popup、property row、asset tile 等基础外观开始统一 +- 后续继续调 UI 观感时,优先改 shared layer,而不是到处修 panel + +### 2. 面板共享控件层 + +已抽出并统一了: + +- toolbar 搜索框 +- toolbar 按钮 / 切换按钮 +- 空状态绘制 +- 面包屑 +- hierarchy tree node +- asset tile +- deferred popup / modal state +- inspector component section +- dialog action row +- property grid / scalar / vector 控件 + +当前结果是 `Hierarchy / Project / Inspector / Console / MenuBar` 的基础表现逻辑已经大面积转向 shared widget,而不是每个 panel 手搓一遍。 + +### 3. Command 层 + +已经从 panel 中抽出了大部分直接业务操作,形成内部命令层: + +- `editor/src/Commands/SceneCommands.h` +- `editor/src/Commands/EntityCommands.h` +- `editor/src/Commands/ProjectCommands.h` +- `editor/src/Commands/ComponentCommands.h` + +当前已进入 command 的行为包括: + +- 新建 / 打开 / 保存场景 +- 启动场景加载与退出时脏场景保存 +- 创建 / 复制 / 粘贴 / Duplicate / 删除 / 重命名 / 重挂接实体 +- 创建文件夹、删除资源、打开资源、移动资源 +- 添加 / 移除组件 + +这一层已经把大量 `undo / dirty / selection reset / scene switch confirm` 从 panel 中剥离出来。 + +### 4. Action / Shortcut 层 + +这一层是 3.26 新补上的,用于承接: + +- 文案 +- shortcut 文本 +- enabled 状态 +- 文本输入时是否允许触发 +- menu 与 shortcut 的统一绑定 +- button / toolbar / inspector action 的统一接线 + +当前文件: + +- `editor/src/Actions/ActionBinding.h` +- `editor/src/Actions/EditorActions.h` + +已覆盖的区域: + +- `MenuBar` +- `Hierarchy` +- `Project` +- `Inspector` +- `Console` + +这一层已经不再只是 `MenuBar + Hierarchy` 的试点,而是开始成为 editor 范围内的统一动作定义入口。 + +### 5. Dock / Layout 层 + +`Dock / Layout` 已从 `EditorLayer::setupDockspace()` 中抽出,形成独立 layout controller: + +- `editor/src/Layout/DockLayoutController.h` + +当前已完成: + +- dock host 渲染与默认布局重建从 `EditorLayer` 中移出 +- `Reset Layout` 已接入真正行为 +- `MenuBar -> EventBus -> DockLayoutController` 的事件链已经打通 +- ImGui layout 已持久化到 `/.xceditor/imgui_layout.ini` + +当前仍待完成: + +- 默认布局参数进一步数据化 +- 更细致的 layout 状态管理 + +### 6. Panel 生命周期层 + +已开始把 `EditorLayer` 内手写的 panel 创建、context 注入、attach、detach、update、event、render 流程,收口到统一 panel collection。 + +当前意义是: + +- `EditorLayer` 不再保存一串分散的 panel 生命周期样板代码 +- panel 的 attach / detach / render 顺序有了统一入口 +- 后续继续拆 panel 或补 panel 时,不需要再改一大片壳层代码 + +## 主要面板状态 + +### MenuBar + +已完成: + +- 文件菜单场景动作走 command +- undo / redo / copy / paste 动作开始统一 +- shortcut 接入 action 层 +- scene 状态显示统一 +- `Reset Layout` 已接入事件驱动行为 +- `About` 已接入真实 modal popup +- `Exit` 已通过事件驱动关闭 editor + +仍待完成: + +- help / app 级动作继续向更统一的 application shell 收口 + +### Hierarchy + +已完成: + +- 搜索栏、节点样式、上下文菜单、创建菜单已共享化 +- 创建 / 删除 / 重命名 / 复制 / 粘贴 / Duplicate / 重挂接走 command +- 快捷键已接 action 层 +- 重命名状态已收成 `Begin / Commit / Cancel` + +仍待完成: + +- 重命名状态机进一步下沉 +- 拖拽 / 空白区域目标等细节继续统一 + +### Project + +已完成: + +- toolbar、breadcrumb、search、asset grid、empty state 已共享化 +- 打开资源、删除资源、创建文件夹、拖拽移动走 command +- 上下文菜单、返回按钮、创建文件夹对话框已接 action 层 +- 资源图标绘制与图标配色已下沉到 shared UI token / widget +- 创建文件夹弹窗已改成 shared popup state 驱动 + +仍待完成: + +- 更多资源对话框 / 多选语义继续统一 + +### Inspector + +已完成: + +- component section 已共享化 +- Add Component 已放到底部 +- 组件增删已走 command +- 组件内容编辑大部分已走 property grid +- Add Component 按钮与 popup 项已接 action 层 + +仍待完成: + +- section header 的动作继续统一 +- 组件编辑交互与 undo 提交边界继续收口 + +### Console + +已完成: + +- 工具栏与内容区布局已共享化 +- 日志行 hover 表现已统一 +- `Clear / Filter` 已接 action 层 + +仍待完成: + +- 控制台状态与动作定义继续统一 + +### Scene / Game + +当前状态: + +- 只保留空壳 panel +- 暂时不做内容 + +这符合当前阶段决策,因为 Viewport 依赖的 RHI 模块仍在重构中。 + +## 还未完成的架构事项 + +### 高优先级 + +1. 整理 `Application.cpp` 和 `EditorLayer.cpp` 中剩余的 UI 壳逻辑 + 包括字体初始化、主 dock host 初始化、部分样式 push / pop 的收口。 + +### 中优先级 + +2. 继续将 panel 的本地状态机抽离 + 包括: + - hierarchy rename state + - project popup / dialog state + - inspector component popup state + - console filter state + +3. 将剩余少量视觉硬编码继续下沉到 token / widget 层 + 尤其是 `ProjectPanel` 中的 icon color 和一部分 panel 局部尺寸。 + +4. 统一 editor 范围内的编辑语义 + 例如: + - 哪些 panel 消费 Copy / Paste / Delete + - 哪些动作受 selection 驱动 + - 哪些动作在文本输入时必须屏蔽 + +### 低优先级 + +5. 再整理一次目录边界 + 当前 `UI / Commands / Actions / Layout` 方向是对的,但后续还可以再决定哪些继续 header-only,哪些拆 `.cpp` 更合适。 + +6. 补一份更正式的 editor 架构说明 + 说明 panel、actions、commands、layout、managers 的职责边界,便于后续继续迭代。 + +7. 根据条件补最小回归测试或最小验证脚本 + 重点验证 shortcut、undo、scene dirty/save switch、drag-drop/reparent。 + +## 下一阶段建议执行顺序 + +### 第一阶段 + +1. 清理 `Application + EditorLayer` UI 壳层 +2. 清理 panel 内剩余局部状态逻辑 + +### 第二阶段 + +3. 收口剩余视觉硬编码 +4. 统一 editor 范围内的编辑语义 + +### 第三阶段 + +5. 根据需要继续细分 `Actions / Commands / Layout` 的目录边界 +6. 补最小回归验证 +7. 为 Viewport 回归预留 editor shell 接口 + +## 阶段性结论 + +当前 editor 重构已经从“到处修视觉 bug”切换到了“逐层收口架构”的状态,方向是正确的。 + +最关键的变化不是某个 panel 看起来更像 Unity,而是已经开始形成稳定分层: + +- `UI` 负责外观与共享控件 +- `Actions` 负责动作定义与 shortcut / button 绑定 +- `Commands` 负责编辑器业务行为 +- `Layout` 负责 dock host 与默认布局 +- `Panels` 逐步退化为纯 UI 壳 + +只要继续沿这个方向收口,后面再做 Scene/Game 接入、Viewport 回归时,成本会明显低于继续在 panel 内部堆逻辑。 diff --git a/editor/resources/Icons/Camera.png b/editor/resources/Icons/Camera.png new file mode 100644 index 00000000..11c81636 Binary files /dev/null and b/editor/resources/Icons/Camera.png differ diff --git a/editor/resources/Icons/cloud.png b/editor/resources/Icons/cloud.png new file mode 100644 index 00000000..45363491 Binary files /dev/null and b/editor/resources/Icons/cloud.png differ diff --git a/editor/resources/Icons/cloud_icon.png b/editor/resources/Icons/cloud_icon.png new file mode 100644 index 00000000..74639606 Binary files /dev/null and b/editor/resources/Icons/cloud_icon.png differ diff --git a/editor/resources/Icons/directory_empty_icon.png b/editor/resources/Icons/directory_empty_icon.png new file mode 100644 index 00000000..93b57c90 Binary files /dev/null and b/editor/resources/Icons/directory_empty_icon.png differ diff --git a/editor/resources/Icons/directory_icon.png b/editor/resources/Icons/directory_icon.png new file mode 100644 index 00000000..68297cff Binary files /dev/null and b/editor/resources/Icons/directory_icon.png differ diff --git a/editor/resources/Icons/image_icon.png b/editor/resources/Icons/image_icon.png new file mode 100644 index 00000000..3a03c0c9 Binary files /dev/null and b/editor/resources/Icons/image_icon.png differ diff --git a/editor/resources/Icons/light.png b/editor/resources/Icons/light.png new file mode 100644 index 00000000..6f235b02 Binary files /dev/null and b/editor/resources/Icons/light.png differ diff --git a/editor/resources/Icons/logo.png b/editor/resources/Icons/logo.png new file mode 100644 index 00000000..f3612e3e Binary files /dev/null and b/editor/resources/Icons/logo.png differ diff --git a/editor/resources/Icons/material_icon.png b/editor/resources/Icons/material_icon.png new file mode 100644 index 00000000..fac4a659 Binary files /dev/null and b/editor/resources/Icons/material_icon.png differ diff --git a/editor/resources/Icons/mesh_icon.png b/editor/resources/Icons/mesh_icon.png new file mode 100644 index 00000000..7828df22 Binary files /dev/null and b/editor/resources/Icons/mesh_icon.png differ diff --git a/editor/resources/Icons/pause_button.png b/editor/resources/Icons/pause_button.png new file mode 100644 index 00000000..1345ae0f Binary files /dev/null and b/editor/resources/Icons/pause_button.png differ diff --git a/editor/resources/Icons/play_button.png b/editor/resources/Icons/play_button.png new file mode 100644 index 00000000..e5d7ef42 Binary files /dev/null and b/editor/resources/Icons/play_button.png differ diff --git a/editor/resources/Icons/scene_icon.png b/editor/resources/Icons/scene_icon.png new file mode 100644 index 00000000..df418d09 Binary files /dev/null and b/editor/resources/Icons/scene_icon.png differ diff --git a/editor/resources/Icons/script_icon.png b/editor/resources/Icons/script_icon.png new file mode 100644 index 00000000..e22124a0 Binary files /dev/null and b/editor/resources/Icons/script_icon.png differ diff --git a/editor/resources/Icons/step_button.png b/editor/resources/Icons/step_button.png new file mode 100644 index 00000000..59630f85 Binary files /dev/null and b/editor/resources/Icons/step_button.png differ diff --git a/editor/resources/Icons/stop_button.png b/editor/resources/Icons/stop_button.png new file mode 100644 index 00000000..ce99b3ca Binary files /dev/null and b/editor/resources/Icons/stop_button.png differ diff --git a/editor/resources/Icons/texture_icon.png b/editor/resources/Icons/texture_icon.png new file mode 100644 index 00000000..4b252717 Binary files /dev/null and b/editor/resources/Icons/texture_icon.png differ diff --git a/editor/resources/Icons/简单模型.png b/editor/resources/Icons/简单模型.png new file mode 100644 index 00000000..65120ae0 Binary files /dev/null and b/editor/resources/Icons/简单模型.png differ diff --git a/editor/src/Actions/ActionBinding.h b/editor/src/Actions/ActionBinding.h new file mode 100644 index 00000000..9765e156 --- /dev/null +++ b/editor/src/Actions/ActionBinding.h @@ -0,0 +1,152 @@ +#pragma once + +#include "UI/UI.h" + +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace Actions { + +struct ShortcutChord { + ImGuiKey key = ImGuiKey_None; + bool ctrl = false; + bool shift = false; + bool alt = false; +}; + +struct ActionBinding { + std::string label; + std::string shortcutLabel; + bool selected = false; + bool enabled = true; + bool allowWhenTextInput = false; + std::array shortcuts{}; + size_t shortcutCount = 0; +}; + +inline ShortcutChord Shortcut(ImGuiKey key, bool ctrl = false, bool shift = false, bool alt = false) { + return ShortcutChord{ key, ctrl, shift, alt }; +} + +inline ActionBinding MakeAction( + std::string label, + const char* shortcutLabel = nullptr, + bool selected = false, + bool enabled = true, + bool allowWhenTextInput = false, + ShortcutChord primaryShortcut = {}, + ShortcutChord secondaryShortcut = {}) { + ActionBinding action; + action.label = std::move(label); + action.shortcutLabel = shortcutLabel ? shortcutLabel : ""; + action.selected = selected; + action.enabled = enabled; + action.allowWhenTextInput = allowWhenTextInput; + + if (primaryShortcut.key != ImGuiKey_None) { + action.shortcuts[action.shortcutCount++] = primaryShortcut; + } + if (secondaryShortcut.key != ImGuiKey_None) { + action.shortcuts[action.shortcutCount++] = secondaryShortcut; + } + + return action; +} + +inline UI::MenuCommand ToMenuCommand(const ActionBinding& action) { + return UI::MenuCommand::Action( + action.label.c_str(), + action.shortcutLabel.empty() ? nullptr : action.shortcutLabel.c_str(), + action.selected, + action.enabled); +} + +template +inline bool DrawMenuAction(const ActionBinding& action, ExecuteFn&& execute) { + return UI::DrawMenuCommand(ToMenuCommand(action), std::forward(execute)); +} + +inline void DrawMenuSeparator() { + UI::DrawMenuCommand(UI::MenuCommand::Separator(), []() {}); +} + +inline bool DrawButtonAction(const ActionBinding& action, ImVec2 size = ImVec2(0.0f, 0.0f)) { + ImGui::BeginDisabled(!action.enabled); + const bool pressed = ImGui::Button(action.label.c_str(), size); + ImGui::EndDisabled(); + return action.enabled && pressed; +} + +inline bool DrawInspectorAction(const ActionBinding& action, ImVec2 size = ImVec2(-1.0f, 0.0f)) { + ImGui::BeginDisabled(!action.enabled); + const bool pressed = UI::InspectorActionButton(action.label.c_str(), size); + ImGui::EndDisabled(); + return action.enabled && pressed; +} + +inline bool DrawToolbarAction(const ActionBinding& action, ImVec2 size = ImVec2(0.0f, 0.0f)) { + ImGui::BeginDisabled(!action.enabled); + const bool pressed = UI::ToolbarButton(action.label.c_str(), action.selected, size); + ImGui::EndDisabled(); + return action.enabled && pressed; +} + +inline bool DrawToolbarToggleAction(const ActionBinding& action, bool& active, ImVec2 size = ImVec2(0.0f, 0.0f)) { + ImGui::BeginDisabled(!action.enabled); + const bool pressed = UI::ToolbarButton(action.label.c_str(), active, size); + ImGui::EndDisabled(); + if (!action.enabled || !pressed) { + return false; + } + + active = !active; + return true; +} + +inline bool IsShortcutPressed(const ShortcutChord& shortcut, const ImGuiIO& io) { + if (shortcut.key == ImGuiKey_None) { + return false; + } + + if (io.KeyCtrl != shortcut.ctrl || io.KeyShift != shortcut.shift || io.KeyAlt != shortcut.alt) { + return false; + } + + return ImGui::IsKeyPressed(shortcut.key, false); +} + +inline bool IsShortcutPressed(const ActionBinding& action) { + if (!action.enabled) { + return false; + } + + const ImGuiIO& io = ImGui::GetIO(); + if (!action.allowWhenTextInput && io.WantTextInput) { + return false; + } + + for (size_t i = 0; i < action.shortcutCount; ++i) { + if (IsShortcutPressed(action.shortcuts[i], io)) { + return true; + } + } + + return false; +} + +template +inline bool HandleShortcut(const ActionBinding& action, ExecuteFn&& execute) { + if (!IsShortcutPressed(action)) { + return false; + } + + std::forward(execute)(); + return true; +} + +} // namespace Actions +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Actions/EditorActions.h b/editor/src/Actions/EditorActions.h new file mode 100644 index 00000000..cda909a7 --- /dev/null +++ b/editor/src/Actions/EditorActions.h @@ -0,0 +1,184 @@ +#pragma once + +#include "ActionBinding.h" +#include "Core/IEditorContext.h" +#include "Core/ISelectionManager.h" +#include "Core/ISceneManager.h" +#include "Core/IUndoManager.h" + +#include + +#include + +namespace XCEngine { +namespace Editor { +namespace Actions { + +inline ActionBinding MakeNewSceneAction() { + return MakeAction("New Scene", "Ctrl+N", false, true, true, Shortcut(ImGuiKey_N, true)); +} + +inline ActionBinding MakeOpenSceneAction() { + return MakeAction("Open Scene", "Ctrl+O", false, true, true, Shortcut(ImGuiKey_O, true)); +} + +inline ActionBinding MakeSaveSceneAction() { + return MakeAction("Save Scene", "Ctrl+S", false, true, true, Shortcut(ImGuiKey_S, true)); +} + +inline ActionBinding MakeSaveSceneAsAction() { + return MakeAction("Save Scene As...", "Ctrl+Shift+S", false, true, true, Shortcut(ImGuiKey_S, true, true)); +} + +inline ActionBinding MakeUndoAction(IEditorContext& context) { + auto& undoManager = context.GetUndoManager(); + const std::string label = undoManager.CanUndo() ? "Undo " + undoManager.GetUndoLabel() : "Undo"; + return MakeAction(label, "Ctrl+Z", false, undoManager.CanUndo(), false, Shortcut(ImGuiKey_Z, true)); +} + +inline ActionBinding MakeRedoAction(IEditorContext& context) { + auto& undoManager = context.GetUndoManager(); + const std::string label = undoManager.CanRedo() ? "Redo " + undoManager.GetRedoLabel() : "Redo"; + return MakeAction( + label, + "Ctrl+Y", + false, + undoManager.CanRedo(), + false, + Shortcut(ImGuiKey_Y, true), + Shortcut(ImGuiKey_Z, true, true)); +} + +inline ActionBinding MakeCutAction(bool enabled = false) { + return MakeAction("Cut", "Ctrl+X", false, enabled, false, Shortcut(ImGuiKey_X, true)); +} + +inline ActionBinding MakeCopyEntityAction(::XCEngine::Components::GameObject* gameObject) { + return MakeAction("Copy", "Ctrl+C", false, gameObject != nullptr, false, Shortcut(ImGuiKey_C, true)); +} + +inline ActionBinding MakePasteEntityAction(IEditorContext& context) { + return MakeAction( + "Paste", + "Ctrl+V", + false, + context.GetSceneManager().HasClipboardData(), + false, + Shortcut(ImGuiKey_V, true)); +} + +inline ActionBinding MakeDuplicateEntityAction(::XCEngine::Components::GameObject* gameObject) { + return MakeAction("Duplicate", "Ctrl+D", false, gameObject != nullptr, false, Shortcut(ImGuiKey_D, true)); +} + +inline ActionBinding MakeDeleteEntityAction(::XCEngine::Components::GameObject* gameObject) { + return MakeAction("Delete", "Delete", false, gameObject != nullptr, false, Shortcut(ImGuiKey_Delete)); +} + +inline ActionBinding MakeRenameEntityAction(::XCEngine::Components::GameObject* gameObject) { + return MakeAction("Rename", "F2", false, gameObject != nullptr, false, Shortcut(ImGuiKey_F2)); +} + +inline ActionBinding MakeCreateChildEntityAction(::XCEngine::Components::GameObject* gameObject) { + return MakeAction("Create Child", nullptr, false, gameObject != nullptr); +} + +inline ActionBinding MakeDetachEntityAction(::XCEngine::Components::GameObject* gameObject) { + return MakeAction( + "Detach from Parent", + nullptr, + false, + gameObject != nullptr && gameObject->GetParent() != nullptr); +} + +inline ActionBinding MakeCreateEmptyEntityAction() { + return MakeAction("Empty Object"); +} + +inline ActionBinding MakeCreateCameraEntityAction() { + return MakeAction("Camera"); +} + +inline ActionBinding MakeCreateLightEntityAction() { + return MakeAction("Light"); +} + +inline ActionBinding MakeCreateCubeEntityAction() { + return MakeAction("Cube"); +} + +inline ActionBinding MakeCreateSphereEntityAction() { + return MakeAction("Sphere"); +} + +inline ActionBinding MakeCreatePlaneEntityAction() { + return MakeAction("Plane"); +} + +inline ActionBinding MakeResetLayoutAction() { + return MakeAction("Reset Layout"); +} + +inline ActionBinding MakeAboutAction() { + return MakeAction("About"); +} + +inline ActionBinding MakeExitAction() { + return MakeAction("Exit", "Alt+F4"); +} + +inline ActionBinding MakeNavigateBackAction(bool enabled) { + return MakeAction("<", nullptr, false, enabled); +} + +inline ActionBinding MakeOpenAssetAction(bool enabled) { + return MakeAction("Open", nullptr, false, enabled); +} + +inline ActionBinding MakeDeleteAssetAction(bool enabled = true) { + return MakeAction("Delete", nullptr, false, enabled); +} + +inline ActionBinding MakeCreateFolderAction(bool enabled = true) { + return MakeAction("Create Folder", nullptr, false, enabled); +} + +inline ActionBinding MakeConfirmCreateAction(bool enabled) { + return MakeAction("Create", nullptr, false, enabled); +} + +inline ActionBinding MakeCancelAction(bool enabled = true) { + return MakeAction("Cancel", nullptr, false, enabled); +} + +inline ActionBinding MakeAddComponentButtonAction(bool enabled = true) { + return MakeAction("Add Component", nullptr, false, enabled); +} + +inline ActionBinding MakeAddComponentMenuAction(const std::string& label, bool enabled) { + return MakeAction(label, nullptr, false, enabled); +} + +inline ActionBinding MakeClearConsoleAction(bool enabled = true) { + return MakeAction("Clear", nullptr, false, enabled); +} + +inline ActionBinding MakeConsoleInfoFilterAction(bool active, bool enabled = true) { + return MakeAction("Info", nullptr, active, enabled); +} + +inline ActionBinding MakeConsoleWarningFilterAction(bool active, bool enabled = true) { + return MakeAction("Warn", nullptr, active, enabled); +} + +inline ActionBinding MakeConsoleErrorFilterAction(bool active, bool enabled = true) { + return MakeAction("Error", nullptr, active, enabled); +} + +inline ::XCEngine::Components::GameObject* GetSelectedGameObject(IEditorContext& context) { + return context.GetSceneManager().GetEntity(context.GetSelectionManager().GetSelectedEntity()); +} + +} // namespace Actions +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Application.cpp b/editor/src/Application.cpp index 22b02883..41ef359f 100644 --- a/editor/src/Application.cpp +++ b/editor/src/Application.cpp @@ -2,6 +2,8 @@ #include "Layers/EditorLayer.h" #include "Core/EditorContext.h" #include "Core/EditorConsoleSink.h" +#include "Core/EditorEvents.h" +#include "Core/EventBus.h" #include #include #include @@ -64,6 +66,10 @@ std::string GetExecutableLogPath(const char* fileName) { return GetExecutableDirectoryUtf8() + "\\" + fileName; } +std::string BuildEditorConfigDirectory(const std::string& projectPath) { + return (std::filesystem::path(projectPath) / ".xceditor").string(); +} + } // namespace static LONG WINAPI GlobalExceptionFilter(EXCEPTION_POINTERS* exceptionPointers) { @@ -128,28 +134,21 @@ bool Application::Initialize(HWND hwnd) { 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(); - + m_editorContext = std::make_shared(); + m_editorContext->SetProjectPath(exeDir); + m_exitRequestedHandlerId = m_editorContext->GetEventBus().Subscribe( + [this](const EditorExitRequestedEvent&) { + if (m_hwnd) { + PostMessageW(m_hwnd, WM_CLOSE, 0, 0); + } + }); + ConfigureImGui(m_editorContext->GetProjectPath()); + ImGui_ImplWin32_Init(hwnd); ImGui_ImplDX12_Init(m_device, 3, DXGI_FORMAT_R8G8B8A8_UNORM, m_srvHeap, m_srvHeap->GetCPUDescriptorHandleForHeapStart(), m_srvHeap->GetGPUDescriptorHandleForHeapStart()); - m_editorContext = std::make_shared(); - m_editorContext->SetProjectPath(exeDir); - m_editorLayer = new EditorLayer(); m_editorLayer->SetContext(m_editorContext); m_layerStack.pushLayer(std::unique_ptr(m_editorLayer)); @@ -160,6 +159,12 @@ bool Application::Initialize(HWND hwnd) { void Application::Shutdown() { m_layerStack.onDetach(); + SaveImGuiSettings(); + + if (m_editorContext && m_exitRequestedHandlerId) { + m_editorContext->GetEventBus().Unsubscribe(m_exitRequestedHandlerId); + m_exitRequestedHandlerId = 0; + } ImGui_ImplDX12_Shutdown(); ImGui_ImplWin32_Shutdown(); @@ -200,7 +205,7 @@ void Application::Render() { barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; m_commandList->ResourceBarrier(1, &barrier); - float clearColor[4] = { 0.12f, 0.12f, 0.12f, 1.0f }; + float clearColor[4] = { 0.22f, 0.22f, 0.22f, 1.0f }; D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = m_rtvHeap->GetCPUDescriptorHandleForHeapStart(); rtvHandle.ptr += m_frameIndex * m_rtvDescriptorSize; m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); @@ -228,6 +233,45 @@ void Application::Render() { } } +void Application::ConfigureImGui(const std::string& projectPath) { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + + ConfigureImGuiIniFile(projectPath, io); + + if (ImFont* uiFont = io.Fonts->AddFontFromFileTTF("C:/Windows/Fonts/msyh.ttc", 15.0f)) { + io.FontDefault = uiFont; + } else { + io.FontDefault = io.Fonts->AddFontDefault(); + } + + unsigned char* pixels = nullptr; + int width = 0; + int height = 0; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + + ApplyUnityDarkTheme(); +} + +void Application::ConfigureImGuiIniFile(const std::string& projectPath, ImGuiIO& io) { + const std::filesystem::path configDir = BuildEditorConfigDirectory(projectPath); + std::error_code ec; + std::filesystem::create_directories(configDir, ec); + + m_imguiIniPath = (configDir / "imgui_layout.ini").string(); + io.IniFilename = m_imguiIniPath.c_str(); +} + +void Application::SaveImGuiSettings() { + if (m_imguiIniPath.empty() || ImGui::GetCurrentContext() == nullptr) { + return; + } + + ImGui::SaveIniSettingsToDisk(m_imguiIniPath.c_str()); +} + void Application::UpdateWindowTitle() { if (!m_hwnd || !m_editorContext) { return; @@ -249,7 +293,7 @@ void Application::UpdateWindowTitle() { sceneName += std::filesystem::path(sceneManager.GetCurrentScenePath()).filename().string(); } - const std::wstring title = Utf8ToWide(sceneName + " - XCVolumeRenderer - Unity Style Editor"); + const std::wstring title = Utf8ToWide(sceneName + " - XCEngine Editor"); if (title != m_lastWindowTitle) { SetWindowTextW(m_hwnd, title.c_str()); m_lastWindowTitle = title; diff --git a/editor/src/Application.h b/editor/src/Application.h index 259db559..3a6626cc 100644 --- a/editor/src/Application.h +++ b/editor/src/Application.h @@ -32,6 +32,9 @@ private: Application() = default; ~Application() = default; + void ConfigureImGui(const std::string& projectPath); + void ConfigureImGuiIniFile(const std::string& projectPath, ImGuiIO& io); + void SaveImGuiSettings(); bool CreateDevice(); bool CreateRenderTarget(); void CleanupRenderTarget(); @@ -57,6 +60,8 @@ private: Core::LayerStack m_layerStack; EditorLayer* m_editorLayer = nullptr; std::shared_ptr m_editorContext; + uint64_t m_exitRequestedHandlerId = 0; + std::string m_imguiIniPath; std::wstring m_lastWindowTitle; }; diff --git a/editor/src/Commands/ComponentCommands.h b/editor/src/Commands/ComponentCommands.h new file mode 100644 index 00000000..58ea8345 --- /dev/null +++ b/editor/src/Commands/ComponentCommands.h @@ -0,0 +1,74 @@ +#pragma once + +#include "ComponentEditors/ComponentEditorRegistry.h" +#include "ComponentEditors/IComponentEditor.h" +#include "Core/IEditorContext.h" +#include "Core/ISceneManager.h" +#include "Utils/UndoUtils.h" + +#include + +namespace XCEngine { +namespace Editor { +namespace Commands { + +inline bool CanAddComponent(const IComponentEditor& editor, ::XCEngine::Components::GameObject* gameObject) { + return gameObject != nullptr && editor.CanAddTo(gameObject); +} + +inline bool AddComponent( + IEditorContext& context, + const IComponentEditor& editor, + ::XCEngine::Components::GameObject* gameObject, + const std::string& commandLabel = {}) { + if (!CanAddComponent(editor, gameObject)) { + return false; + } + + bool added = false; + const std::string label = + commandLabel.empty() ? std::string("Add ") + editor.GetDisplayName() + " Component" : commandLabel; + UndoUtils::ExecuteSceneCommand(context, label, [&]() { + added = editor.AddTo(gameObject) != nullptr; + if (added) { + context.GetSceneManager().MarkSceneDirty(); + } + }); + return added; +} + +inline bool CanRemoveComponent( + ::XCEngine::Components::Component* component, + const IComponentEditor* editor = nullptr) { + if (!component) { + return false; + } + + const IComponentEditor* resolvedEditor = + editor ? editor : ComponentEditorRegistry::Get().FindEditor(component); + return resolvedEditor != nullptr && resolvedEditor->CanRemove(component); +} + +inline bool RemoveComponent( + IEditorContext& context, + ::XCEngine::Components::Component* component, + ::XCEngine::Components::GameObject* gameObject, + const IComponentEditor* editor = nullptr, + const std::string& commandLabel = {}) { + if (!gameObject || !CanRemoveComponent(component, editor)) { + return false; + } + + const std::string label = commandLabel.empty() + ? std::string("Remove ") + component->GetName() + " Component" + : commandLabel; + UndoUtils::ExecuteSceneCommand(context, label, [&]() { + gameObject->RemoveComponent(component); + context.GetSceneManager().MarkSceneDirty(); + }); + return true; +} + +} // namespace Commands +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Commands/EntityCommands.h b/editor/src/Commands/EntityCommands.h new file mode 100644 index 00000000..a749ec40 --- /dev/null +++ b/editor/src/Commands/EntityCommands.h @@ -0,0 +1,217 @@ +#pragma once + +#include "Core/IEditorContext.h" +#include "Core/ISelectionManager.h" +#include "Core/ISceneManager.h" +#include "Utils/UndoUtils.h" + +#include +#include +#include +#include + +#include + +namespace XCEngine { +namespace Editor { +namespace Commands { + +struct NoopEntitySetup { + void operator()(::XCEngine::Components::GameObject&, ISceneManager&) const { + } +}; + +template +inline ::XCEngine::Components::GameObject* CreateEntity( + IEditorContext& context, + const std::string& commandLabel, + const std::string& entityName, + ::XCEngine::Components::GameObject* parent = nullptr, + SetupFn setup = {}) { + ::XCEngine::Components::GameObject* created = nullptr; + UndoUtils::ExecuteSceneCommand(context, commandLabel, [&]() { + auto& sceneManager = context.GetSceneManager(); + created = sceneManager.CreateEntity(entityName, parent); + if (!created) { + return; + } + + setup(*created, sceneManager); + context.GetSelectionManager().SetSelectedEntity(created->GetID()); + }); + return created; +} + +inline ::XCEngine::Components::GameObject* CreateEmptyEntity( + IEditorContext& context, + ::XCEngine::Components::GameObject* parent = nullptr, + const std::string& commandLabel = "Create Entity", + const std::string& entityName = "GameObject") { + return CreateEntity(context, commandLabel, entityName, parent); +} + +inline ::XCEngine::Components::GameObject* CreateCameraEntity( + IEditorContext& context, + ::XCEngine::Components::GameObject* parent = nullptr, + const std::string& commandLabel = "Create Camera", + const std::string& entityName = "Camera") { + return CreateEntity( + context, + commandLabel, + entityName, + parent, + [](::XCEngine::Components::GameObject& entity, ISceneManager&) { + entity.AddComponent<::XCEngine::Components::CameraComponent>(); + }); +} + +inline ::XCEngine::Components::GameObject* CreateLightEntity( + IEditorContext& context, + ::XCEngine::Components::GameObject* parent = nullptr, + const std::string& commandLabel = "Create Light", + const std::string& entityName = "Light") { + return CreateEntity( + context, + commandLabel, + entityName, + parent, + [](::XCEngine::Components::GameObject& entity, ISceneManager&) { + entity.AddComponent<::XCEngine::Components::LightComponent>(); + }); +} + +inline bool RenameEntity( + IEditorContext& context, + ::XCEngine::Components::GameObject::ID entityId, + const std::string& newName, + const std::string& commandLabel = "Rename Entity") { + if (newName.empty() || !context.GetSceneManager().GetEntity(entityId)) { + return false; + } + + UndoUtils::ExecuteSceneCommand(context, commandLabel, [&]() { + context.GetSceneManager().RenameEntity(entityId, newName); + }); + return true; +} + +inline bool DeleteEntity( + IEditorContext& context, + ::XCEngine::Components::GameObject::ID entityId, + const std::string& commandLabel = "Delete Entity") { + if (!context.GetSceneManager().GetEntity(entityId)) { + return false; + } + + UndoUtils::ExecuteSceneCommand(context, commandLabel, [&]() { + context.GetSceneManager().DeleteEntity(entityId); + }); + return true; +} + +inline bool CopyEntity(IEditorContext& context, ::XCEngine::Components::GameObject::ID entityId) { + if (!context.GetSceneManager().GetEntity(entityId)) { + return false; + } + + context.GetSceneManager().CopyEntity(entityId); + return true; +} + +inline ::XCEngine::Components::GameObject::ID PasteEntity( + IEditorContext& context, + ::XCEngine::Components::GameObject::ID parentId = 0, + const std::string& commandLabel = "Paste Entity") { + ::XCEngine::Components::GameObject::ID newId = 0; + UndoUtils::ExecuteSceneCommand(context, commandLabel, [&]() { + newId = context.GetSceneManager().PasteEntity(parentId); + if (newId != 0) { + context.GetSelectionManager().SetSelectedEntity(newId); + } + }); + return newId; +} + +inline ::XCEngine::Components::GameObject::ID DuplicateEntity( + IEditorContext& context, + ::XCEngine::Components::GameObject::ID entityId, + const std::string& commandLabel = "Duplicate Entity") { + if (!context.GetSceneManager().GetEntity(entityId)) { + return 0; + } + + ::XCEngine::Components::GameObject::ID newId = 0; + UndoUtils::ExecuteSceneCommand(context, commandLabel, [&]() { + newId = context.GetSceneManager().DuplicateEntity(entityId); + if (newId != 0) { + context.GetSelectionManager().SetSelectedEntity(newId); + } + }); + return newId; +} + +inline bool CanReparentEntity( + const ::XCEngine::Components::GameObject* source, + const ::XCEngine::Components::GameObject* newParent) { + if (!source) { + return false; + } + + for (auto* current = newParent; current != nullptr; current = current->GetParent()) { + if (current == source) { + return false; + } + } + + return true; +} + +inline bool ReparentEntityPreserveWorldTransform( + IEditorContext& context, + ::XCEngine::Components::GameObject* source, + ::XCEngine::Components::GameObject::ID newParentId, + const std::string& commandLabel = "Reparent Entity") { + if (!source) { + return false; + } + + const ::XCEngine::Components::GameObject::ID currentParentId = + source->GetParent() ? source->GetParent()->GetID() : 0; + if (currentParentId == newParentId) { + return false; + } + + UndoUtils::ExecuteSceneCommand(context, commandLabel, [&]() { + auto& sceneManager = context.GetSceneManager(); + auto* transform = source->GetTransform(); + if (!transform) { + sceneManager.MoveEntity(source->GetID(), newParentId); + return; + } + + const ::XCEngine::Math::Vector3 worldPos = transform->GetPosition(); + const ::XCEngine::Math::Quaternion worldRot = transform->GetRotation(); + const ::XCEngine::Math::Vector3 worldScale = transform->GetScale(); + + sceneManager.MoveEntity(source->GetID(), newParentId); + transform->SetPosition(worldPos); + transform->SetRotation(worldRot); + transform->SetScale(worldScale); + }); + return true; +} + +inline bool DetachEntity( + IEditorContext& context, + ::XCEngine::Components::GameObject* entity, + const std::string& commandLabel = "Reparent Entity") { + if (!entity || entity->GetParent() == nullptr) { + return false; + } + + return ReparentEntityPreserveWorldTransform(context, entity, 0, commandLabel); +} + +} // namespace Commands +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Commands/ProjectCommands.h b/editor/src/Commands/ProjectCommands.h new file mode 100644 index 00000000..00860962 --- /dev/null +++ b/editor/src/Commands/ProjectCommands.h @@ -0,0 +1,66 @@ +#pragma once + +#include "Core/AssetItem.h" +#include "Core/IEditorContext.h" +#include "Core/IProjectManager.h" +#include "SceneCommands.h" + +#include + +namespace XCEngine { +namespace Editor { +namespace Commands { + +inline bool CanOpenAsset(const AssetItemPtr& item) { + return item != nullptr && (item->isFolder || item->type == "Scene"); +} + +inline bool OpenAsset(IEditorContext& context, const AssetItemPtr& item) { + if (!CanOpenAsset(item)) { + return false; + } + + if (item->isFolder) { + context.GetProjectManager().NavigateToFolder(item); + return true; + } + + return LoadScene(context, item->fullPath); +} + +inline bool CreateFolder(IProjectManager& projectManager, const std::string& name) { + if (name.empty()) { + return false; + } + + projectManager.CreateFolder(name); + return true; +} + +inline bool DeleteAsset(IProjectManager& projectManager, int index) { + if (index < 0) { + return false; + } + + projectManager.DeleteItem(index); + return true; +} + +inline bool MoveAssetToFolder( + IProjectManager& projectManager, + const std::string& sourceFullPath, + const AssetItemPtr& targetFolder) { + if (sourceFullPath.empty() || !targetFolder || !targetFolder->isFolder) { + return false; + } + + if (sourceFullPath == targetFolder->fullPath) { + return false; + } + + return projectManager.MoveItem(sourceFullPath, targetFolder->fullPath); +} + +} // namespace Commands +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Commands/SceneCommands.h b/editor/src/Commands/SceneCommands.h new file mode 100644 index 00000000..c9b6783d --- /dev/null +++ b/editor/src/Commands/SceneCommands.h @@ -0,0 +1,108 @@ +#pragma once + +#include "Core/IEditorContext.h" +#include "Core/IProjectManager.h" +#include "Core/ISelectionManager.h" +#include "Core/ISceneManager.h" +#include "Core/IUndoManager.h" +#include "Utils/SceneEditorUtils.h" + +#include + +namespace XCEngine { +namespace Editor { +namespace Commands { + +inline void ResetSceneEditingState(IEditorContext& context) { + context.GetSelectionManager().ClearSelection(); + context.GetUndoManager().ClearHistory(); +} + +inline bool NewScene(IEditorContext& context, const std::string& sceneName = "Untitled Scene") { + if (!SceneEditorUtils::ConfirmSceneSwitch(context)) { + return false; + } + + context.GetSceneManager().NewScene(sceneName); + ResetSceneEditingState(context); + return true; +} + +inline bool LoadScene(IEditorContext& context, const std::string& filePath, bool confirmSwitch = true) { + if (filePath.empty()) { + return false; + } + if (confirmSwitch && !SceneEditorUtils::ConfirmSceneSwitch(context)) { + return false; + } + if (!context.GetSceneManager().LoadScene(filePath)) { + return false; + } + + ResetSceneEditingState(context); + return true; +} + +inline bool OpenSceneWithDialog(IEditorContext& context) { + if (!SceneEditorUtils::ConfirmSceneSwitch(context)) { + return false; + } + + const std::string filePath = SceneEditorUtils::OpenSceneFileDialog( + context.GetProjectPath(), + context.GetSceneManager().GetCurrentScenePath()); + if (filePath.empty()) { + return false; + } + + return LoadScene(context, filePath, false); +} + +inline bool SaveCurrentScene(IEditorContext& context) { + return SceneEditorUtils::SaveCurrentScene(context); +} + +inline bool SaveSceneAsWithDialog(IEditorContext& context) { + auto& sceneManager = context.GetSceneManager(); + const std::string filePath = SceneEditorUtils::SaveSceneFileDialog( + context.GetProjectPath(), + sceneManager.GetCurrentScenePath(), + sceneManager.GetCurrentSceneName()); + if (filePath.empty()) { + return false; + } + + const bool saved = sceneManager.SaveSceneAs(filePath); + if (saved) { + context.GetProjectManager().RefreshCurrentFolder(); + } + return saved; +} + +inline bool LoadStartupScene(IEditorContext& context) { + const bool loaded = context.GetSceneManager().LoadStartupScene(context.GetProjectPath()); + context.GetProjectManager().RefreshCurrentFolder(); + ResetSceneEditingState(context); + return loaded; +} + +inline bool SaveDirtySceneWithFallback(IEditorContext& context, const std::string& fallbackPath) { + auto& sceneManager = context.GetSceneManager(); + if (!sceneManager.HasActiveScene() || !sceneManager.IsSceneDirty()) { + return true; + } + + if (sceneManager.SaveScene()) { + return true; + } + + if (fallbackPath.empty()) { + return false; + } + + return sceneManager.SaveSceneAs(fallbackPath); +} + +} // namespace Commands +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/ComponentEditors/CameraComponentEditor.h b/editor/src/ComponentEditors/CameraComponentEditor.h index 88585941..fa6740ce 100644 --- a/editor/src/ComponentEditors/CameraComponentEditor.h +++ b/editor/src/ComponentEditors/CameraComponentEditor.h @@ -25,72 +25,75 @@ public: return false; } + constexpr const char* kUndoLabel = "Modify Camera"; + int projectionType = static_cast(camera->GetProjectionType()); const char* projectionLabels[] = { "Perspective", "Orthographic" }; bool changed = false; - if (ImGui::Combo("Projection", &projectionType, projectionLabels, 2)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Camera"); - } - camera->SetProjectionType(static_cast<::XCEngine::Components::CameraProjectionType>(projectionType)); - changed = true; - } + const int newProjectionType = UI::DrawPropertyCombo("Projection", projectionType, projectionLabels, 2); + changed |= UI::ApplyPropertyChange( + newProjectionType != projectionType, + undoManager, + kUndoLabel, + [&]() { + camera->SetProjectionType(static_cast<::XCEngine::Components::CameraProjectionType>(newProjectionType)); + }); if (camera->GetProjectionType() == ::XCEngine::Components::CameraProjectionType::Perspective) { float fieldOfView = camera->GetFieldOfView(); - if (UI::DrawSliderFloat("Field Of View", fieldOfView, 1.0f, 179.0f, 100.0f, "%.1f")) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Camera"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertySliderFloat("Field Of View", fieldOfView, 1.0f, 179.0f, "%.1f"), + undoManager, + kUndoLabel, + [&]() { camera->SetFieldOfView(fieldOfView); - changed = true; - } + }); } else { float orthographicSize = camera->GetOrthographicSize(); - if (UI::DrawFloat("Orthographic Size", orthographicSize, 100.0f, 0.1f, 0.001f)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Camera"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyFloat("Orthographic Size", orthographicSize, 0.1f, 0.001f), + undoManager, + kUndoLabel, + [&]() { camera->SetOrthographicSize(orthographicSize); - changed = true; - } + }); } float nearClip = camera->GetNearClipPlane(); - if (UI::DrawFloat("Near Clip", nearClip, 100.0f, 0.01f, 0.001f)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Camera"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyFloat("Near Clip", nearClip, 0.01f, 0.001f), + undoManager, + kUndoLabel, + [&]() { camera->SetNearClipPlane(nearClip); - changed = true; - } + }); float farClip = camera->GetFarClipPlane(); - if (UI::DrawFloat("Far Clip", farClip, 100.0f, 0.1f, nearClip + 0.001f)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Camera"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyFloat("Far Clip", farClip, 0.1f, nearClip + 0.001f), + undoManager, + kUndoLabel, + [&]() { camera->SetFarClipPlane(farClip); - changed = true; - } + }); float depth = camera->GetDepth(); - if (UI::DrawFloat("Depth", depth, 100.0f, 0.1f)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Camera"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyFloat("Depth", depth, 0.1f), + undoManager, + kUndoLabel, + [&]() { camera->SetDepth(depth); - changed = true; - } + }); bool primary = camera->IsPrimary(); - if (UI::DrawBool("Primary", primary)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Camera"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyBool("Primary", primary), + undoManager, + kUndoLabel, + [&]() { camera->SetPrimary(primary); - changed = true; - } + }); float clearColor[4] = { camera->GetClearColor().r, @@ -98,13 +101,13 @@ public: camera->GetClearColor().b, camera->GetClearColor().a }; - if (UI::DrawColor4("Clear Color", clearColor)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Camera"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyColor4("Clear Color", clearColor), + undoManager, + kUndoLabel, + [&]() { camera->SetClearColor(::XCEngine::Math::Color(clearColor[0], clearColor[1], clearColor[2], clearColor[3])); - changed = true; - } + }); return changed; } diff --git a/editor/src/ComponentEditors/LightComponentEditor.h b/editor/src/ComponentEditors/LightComponentEditor.h index 01bd8d68..9f545226 100644 --- a/editor/src/ComponentEditors/LightComponentEditor.h +++ b/editor/src/ComponentEditors/LightComponentEditor.h @@ -25,16 +25,19 @@ public: return false; } + constexpr const char* kUndoLabel = "Modify Light"; + int lightType = static_cast(light->GetLightType()); const char* lightTypeLabels[] = { "Directional", "Point", "Spot" }; bool changed = false; - if (ImGui::Combo("Type", &lightType, lightTypeLabels, 3)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Light"); - } - light->SetLightType(static_cast<::XCEngine::Components::LightType>(lightType)); - changed = true; - } + const int newLightType = UI::DrawPropertyCombo("Type", lightType, lightTypeLabels, 3); + changed |= UI::ApplyPropertyChange( + newLightType != lightType, + undoManager, + kUndoLabel, + [&]() { + light->SetLightType(static_cast<::XCEngine::Components::LightType>(newLightType)); + }); float color[4] = { light->GetColor().r, @@ -42,53 +45,53 @@ public: light->GetColor().b, light->GetColor().a }; - if (UI::DrawColor4("Color", color)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Light"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyColor4("Color", color), + undoManager, + kUndoLabel, + [&]() { light->SetColor(::XCEngine::Math::Color(color[0], color[1], color[2], color[3])); - changed = true; - } + }); float intensity = light->GetIntensity(); - if (UI::DrawFloat("Intensity", intensity, 100.0f, 0.1f, 0.0f)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Light"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyFloat("Intensity", intensity, 0.1f, 0.0f), + undoManager, + kUndoLabel, + [&]() { light->SetIntensity(intensity); - changed = true; - } + }); if (light->GetLightType() != ::XCEngine::Components::LightType::Directional) { float range = light->GetRange(); - if (UI::DrawFloat("Range", range, 100.0f, 0.1f, 0.001f)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Light"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyFloat("Range", range, 0.1f, 0.001f), + undoManager, + kUndoLabel, + [&]() { light->SetRange(range); - changed = true; - } + }); } if (light->GetLightType() == ::XCEngine::Components::LightType::Spot) { float spotAngle = light->GetSpotAngle(); - if (UI::DrawSliderFloat("Spot Angle", spotAngle, 1.0f, 179.0f, 100.0f, "%.1f")) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Light"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertySliderFloat("Spot Angle", spotAngle, 1.0f, 179.0f, "%.1f"), + undoManager, + kUndoLabel, + [&]() { light->SetSpotAngle(spotAngle); - changed = true; - } + }); } bool castsShadows = light->GetCastsShadows(); - if (UI::DrawBool("Cast Shadows", castsShadows)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Light"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyBool("Cast Shadows", castsShadows), + undoManager, + kUndoLabel, + [&]() { light->SetCastsShadows(castsShadows); - changed = true; - } + }); return changed; } diff --git a/editor/src/ComponentEditors/TransformComponentEditor.h b/editor/src/ComponentEditors/TransformComponentEditor.h index 56854b0d..d5acf68b 100644 --- a/editor/src/ComponentEditors/TransformComponentEditor.h +++ b/editor/src/ComponentEditors/TransformComponentEditor.h @@ -5,6 +5,8 @@ #include "UI/UI.h" #include +#include +#include namespace XCEngine { namespace Editor { @@ -25,34 +27,46 @@ public: return false; } + constexpr const char* kUndoLabel = "Modify Transform"; + + RotationEditState& rotationState = m_rotationStates[transform]; ::XCEngine::Math::Vector3 position = transform->GetLocalPosition(); - ::XCEngine::Math::Vector3 rotation = transform->GetLocalEulerAngles(); ::XCEngine::Math::Vector3 scale = transform->GetLocalScale(); + const ::XCEngine::Math::Quaternion currentRotation = transform->GetLocalRotation(); + if (!rotationState.initialized || (!rotationState.isEditing && !SameRotation(currentRotation, rotationState.lastRotation))) { + rotationState.displayedEuler = transform->GetLocalEulerAngles(); + rotationState.lastRotation = currentRotation; + rotationState.initialized = true; + } + bool changed = false; - if (UI::DrawVec3("Position", position, 0.0f, 80.0f, 0.1f)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Transform"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyVec3Input("Position", position, 0.1f), + undoManager, + kUndoLabel, + [&]() { transform->SetLocalPosition(position); - changed = true; - } + }); - if (UI::DrawVec3("Rotation", rotation, 0.0f, 80.0f, 1.0f)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Transform"); - } - transform->SetLocalEulerAngles(rotation); - changed = true; - } + bool rotationActive = false; + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyVec3Input("Rotation", rotationState.displayedEuler, 1.0f, &rotationActive), + undoManager, + kUndoLabel, + [&]() { + transform->SetLocalEulerAngles(rotationState.displayedEuler); + rotationState.lastRotation = transform->GetLocalRotation(); + }); + rotationState.isEditing = rotationActive; - if (UI::DrawVec3("Scale", scale, 1.0f, 80.0f, 0.1f)) { - if (undoManager) { - undoManager->BeginInteractiveChange("Modify Transform"); - } + changed |= UI::ApplyPropertyChange( + UI::DrawPropertyVec3Input("Scale", scale, 0.1f), + undoManager, + kUndoLabel, + [&]() { transform->SetLocalScale(scale); - changed = true; - } + }); return changed; } @@ -71,6 +85,20 @@ public: (void)component; return false; } + +private: + struct RotationEditState { + ::XCEngine::Math::Vector3 displayedEuler = ::XCEngine::Math::Vector3::Zero(); + ::XCEngine::Math::Quaternion lastRotation = ::XCEngine::Math::Quaternion::Identity(); + bool initialized = false; + bool isEditing = false; + }; + + static bool SameRotation(const ::XCEngine::Math::Quaternion& a, const ::XCEngine::Math::Quaternion& b) { + return std::abs(a.Dot(b)) > 0.9999f; + } + + std::unordered_map<::XCEngine::Components::TransformComponent*, RotationEditState> m_rotationStates; }; } // namespace Editor diff --git a/editor/src/Core/EditorEvents.h b/editor/src/Core/EditorEvents.h index ba59e746..6c6fd8cf 100644 --- a/editor/src/Core/EditorEvents.h +++ b/editor/src/Core/EditorEvents.h @@ -48,5 +48,11 @@ struct EditorModeChangedEvent { int newMode; }; +struct DockLayoutResetRequestedEvent { +}; + +struct EditorExitRequestedEvent { +}; + } } diff --git a/editor/src/Layers/EditorLayer.cpp b/editor/src/Layers/EditorLayer.cpp index 8fc0d99b..24dd691a 100644 --- a/editor/src/Layers/EditorLayer.cpp +++ b/editor/src/Layers/EditorLayer.cpp @@ -1,4 +1,6 @@ +#include "Commands/SceneCommands.h" #include "EditorLayer.h" +#include "Layout/DockLayoutController.h" #include "panels/MenuBar.h" #include "panels/HierarchyPanel.h" #include "panels/SceneViewPanel.h" @@ -9,14 +11,20 @@ #include "Core/IEditorContext.h" #include "Core/EditorContext.h" #include "Core/IUndoManager.h" -#include "UI/DockHostStyle.h" #include #include -#include namespace XCEngine { namespace Editor { +namespace { + +std::string BuildFallbackScenePath(const IEditorContext& context) { + return (std::filesystem::path(context.GetProjectPath()) / "Assets" / "Scenes" / "Main.xc").string(); +} + +} // namespace + EditorLayer::EditorLayer() : Layer("Editor") {} void EditorLayer::SetContext(std::shared_ptr context) { @@ -27,138 +35,49 @@ void EditorLayer::onAttach() { if (!m_context) { m_context = std::make_shared(); } - - 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(); - - m_menuBar->SetContext(m_context.get()); - m_hierarchyPanel->SetContext(m_context.get()); - m_sceneViewPanel->SetContext(m_context.get()); - m_gameViewPanel->SetContext(m_context.get()); - m_inspectorPanel->SetContext(m_context.get()); - m_consolePanel->SetContext(m_context.get()); - m_projectPanel->SetContext(m_context.get()); - - m_projectPanel->Initialize(m_context->GetProjectPath()); - m_context->GetSceneManager().LoadStartupScene(m_context->GetProjectPath()); - m_context->GetProjectManager().RefreshCurrentFolder(); - m_context->GetUndoManager().ClearHistory(); - m_menuBar->OnAttach(); - m_hierarchyPanel->OnAttach(); - m_sceneViewPanel->OnAttach(); - m_gameViewPanel->OnAttach(); - m_inspectorPanel->OnAttach(); - m_consolePanel->OnAttach(); - m_projectPanel->OnAttach(); + m_panels.Clear(); + m_panels.SetContext(m_context.get()); + m_panels.Emplace(); + m_panels.Emplace(); + m_panels.Emplace(); + m_panels.Emplace(); + m_panels.Emplace(); + m_panels.Emplace(); + m_projectPanel = &m_panels.Emplace(); + m_dockLayoutController = std::make_unique(); + + m_projectPanel->Initialize(m_context->GetProjectPath()); + Commands::LoadStartupScene(*m_context); + m_dockLayoutController->Attach(*m_context); + m_panels.AttachAll(); } void EditorLayer::onDetach() { - auto& sceneManager = m_context->GetSceneManager(); - if (sceneManager.HasActiveScene() && sceneManager.IsSceneDirty()) { - if (!sceneManager.SaveScene()) { - const std::string fallbackPath = - (std::filesystem::path(m_context->GetProjectPath()) / "Assets" / "Scenes" / "Main.xc").string(); - sceneManager.SaveSceneAs(fallbackPath); - } + if (m_context) { + Commands::SaveDirtySceneWithFallback(*m_context, BuildFallbackScenePath(*m_context)); } - m_menuBar->OnDetach(); - m_hierarchyPanel->OnDetach(); - m_sceneViewPanel->OnDetach(); - m_gameViewPanel->OnDetach(); - m_inspectorPanel->OnDetach(); - m_consolePanel->OnDetach(); - m_projectPanel->OnDetach(); + if (m_dockLayoutController) { + m_dockLayoutController->Detach(); + } + + m_panels.DetachAll(); + m_panels.Clear(); + m_projectPanel = nullptr; } void EditorLayer::onUpdate(float dt) { + m_panels.UpdateAll(dt); } void EditorLayer::onEvent(void* event) { - ImGuiIO& io = ImGui::GetIO(); - - // TODO: These functions don't exist - need to implement them - // if (ImGui::IsKeyPressed(ImGuiKey_F5)) { - // TogglePlay(); - // } - // - // if (ImGui::IsKeyPressed(ImGuiKey_F6)) { - // if (GetEditorMode() != EditorMode::Edit) { - // TogglePause(); - // } - // } + m_panels.DispatchEvent(event); } void EditorLayer::onImGuiRender() { - setupDockspace(); - renderAllPanels(); -} - -void EditorLayer::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"); - { - UI::DockHostStyleScope dockHostStyle; - 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 EditorLayer::renderAllPanels() { - m_menuBar->Render(); - - m_hierarchyPanel->Render(); - m_sceneViewPanel->Render(); - m_gameViewPanel->Render(); - m_inspectorPanel->Render(); - m_consolePanel->Render(); - m_projectPanel->Render(); + m_dockLayoutController->RenderDockspace(); + m_panels.RenderAll(); } } diff --git a/editor/src/Layers/EditorLayer.h b/editor/src/Layers/EditorLayer.h index 5f047199..9257e27d 100644 --- a/editor/src/Layers/EditorLayer.h +++ b/editor/src/Layers/EditorLayer.h @@ -1,5 +1,7 @@ #pragma once +#include "panels/PanelCollection.h" + #include #include #include @@ -16,6 +18,7 @@ class GameViewPanel; class InspectorPanel; class ConsolePanel; class ProjectPanel; +class DockLayoutController; class EditorLayer : public Core::Layer { public: @@ -31,18 +34,10 @@ public: void SetContext(std::shared_ptr context); private: - void setupDockspace(); - void renderAllPanels(); - std::shared_ptr m_context; - - 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; + std::unique_ptr m_dockLayoutController; + PanelCollection m_panels; + ProjectPanel* m_projectPanel = nullptr; }; } diff --git a/editor/src/Layout/DockLayoutController.h b/editor/src/Layout/DockLayoutController.h new file mode 100644 index 00000000..fc2be440 --- /dev/null +++ b/editor/src/Layout/DockLayoutController.h @@ -0,0 +1,101 @@ +#pragma once + +#include "Core/EditorEvents.h" +#include "Core/EventBus.h" +#include "Core/IEditorContext.h" +#include "UI/DockHostStyle.h" + +#include +#include + +namespace XCEngine { +namespace Editor { + +class DockLayoutController { +public: + void Attach(IEditorContext& context) { + if (m_context == &context) { + return; + } + + m_context = &context; + m_resetLayoutHandlerId = context.GetEventBus().Subscribe( + [this](const DockLayoutResetRequestedEvent&) { + RequestReset(); + }); + } + + void Detach() { + if (m_context && m_resetLayoutHandlerId) { + m_context->GetEventBus().Unsubscribe(m_resetLayoutHandlerId); + } + + m_context = nullptr; + m_resetLayoutHandlerId = 0; + m_layoutDirty = true; + } + + void RequestReset() { + m_layoutDirty = true; + } + + void RenderDockspace() { + 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); + + const ImGuiID dockspaceId = ImGui::GetID("MainDockspace.Root"); + { + UI::DockHostStyleScope dockHostStyle; + ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), m_dockspaceFlags); + } + + if (m_layoutDirty) { + BuildDefaultLayout(dockspaceId, viewport->Size); + m_layoutDirty = false; + } + + ImGui::End(); + } + +private: + void BuildDefaultLayout(ImGuiID dockspaceId, const ImVec2& dockspaceSize) { + ImGui::DockBuilderRemoveNode(dockspaceId); + ImGui::DockBuilderAddNode(dockspaceId, m_dockspaceFlags | ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(dockspaceId, dockspaceSize); + + 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); + } + + IEditorContext* m_context = nullptr; + uint64_t m_resetLayoutHandlerId = 0; + bool m_layoutDirty = true; + ImGuiDockNodeFlags m_dockspaceFlags = ImGuiDockNodeFlags_NoWindowMenuButton; +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Theme.cpp b/editor/src/Theme.cpp index 87464c02..80865549 100644 --- a/editor/src/Theme.cpp +++ b/editor/src/Theme.cpp @@ -1,97 +1,12 @@ #include "Theme.h" -#include "UI/StyleTokens.h" +#include "UI/BaseTheme.h" #include namespace XCEngine { namespace Editor { void ApplyUnityDarkTheme() { - ImGuiStyle& style = ImGui::GetStyle(); - ImVec4* colors = style.Colors; - - colors[ImGuiCol_Text] = ImVec4(0.80f, 0.80f, 0.80f, 1.00f); - colors[ImGuiCol_TextDisabled] = ImVec4(0.53f, 0.53f, 0.53f, 1.00f); - colors[ImGuiCol_WindowBg] = ImVec4(0.22f, 0.22f, 0.22f, 1.00f); - colors[ImGuiCol_ChildBg] = ImVec4(0.22f, 0.22f, 0.22f, 1.00f); - colors[ImGuiCol_PopupBg] = ImVec4(0.17f, 0.17f, 0.17f, 0.98f); - colors[ImGuiCol_Border] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); - colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_FrameBg] = ImVec4(0.18f, 0.18f, 0.18f, 1.00f); - colors[ImGuiCol_FrameBgHovered] = ImVec4(0.21f, 0.21f, 0.21f, 1.00f); - colors[ImGuiCol_FrameBgActive] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); - colors[ImGuiCol_TitleBg] = ImVec4(0.18f, 0.18f, 0.18f, 1.00f); - colors[ImGuiCol_TitleBgActive] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); - colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.18f, 0.18f, 0.18f, 1.00f); - colors[ImGuiCol_MenuBarBg] = ImVec4(0.19f, 0.19f, 0.19f, 1.00f); - colors[ImGuiCol_ScrollbarBg] = ImVec4(0.16f, 0.16f, 0.16f, 1.00f); - colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f); - colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.36f, 0.36f, 0.36f, 1.00f); - colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.41f, 0.41f, 0.41f, 1.00f); - colors[ImGuiCol_CheckMark] = ImVec4(0.72f, 0.72f, 0.72f, 1.00f); - colors[ImGuiCol_SliderGrab] = ImVec4(0.44f, 0.44f, 0.44f, 1.00f); - colors[ImGuiCol_SliderGrabActive] = ImVec4(0.54f, 0.54f, 0.54f, 1.00f); - colors[ImGuiCol_Button] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); - colors[ImGuiCol_ButtonHovered] = ImVec4(0.28f, 0.28f, 0.28f, 1.00f); - colors[ImGuiCol_ButtonActive] = ImVec4(0.31f, 0.31f, 0.31f, 1.00f); - colors[ImGuiCol_Header] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); - colors[ImGuiCol_HeaderHovered] = ImVec4(0.27f, 0.27f, 0.27f, 1.00f); - colors[ImGuiCol_HeaderActive] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f); - colors[ImGuiCol_Separator] = ImVec4(0.13f, 0.13f, 0.13f, 1.00f); - colors[ImGuiCol_SeparatorHovered] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f); - colors[ImGuiCol_SeparatorActive] = ImVec4(0.34f, 0.34f, 0.34f, 1.00f); - colors[ImGuiCol_ResizeGrip] = ImVec4(0.24f, 0.24f, 0.24f, 0.00f); - colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.36f, 0.36f, 0.36f, 0.25f); - colors[ImGuiCol_ResizeGripActive] = ImVec4(0.52f, 0.52f, 0.52f, 0.25f); - colors[ImGuiCol_Tab] = UI::DockTabColor(); - colors[ImGuiCol_TabHovered] = UI::DockTabHoveredColor(); - colors[ImGuiCol_TabSelected] = UI::DockTabSelectedColor(); - colors[ImGuiCol_TabSelectedOverline] = UI::DockTabSelectedOverlineColor(); - colors[ImGuiCol_TabDimmed] = UI::DockTabDimmedColor(); - colors[ImGuiCol_TabDimmedSelected] = UI::DockTabDimmedSelectedColor(); - colors[ImGuiCol_TabDimmedSelectedOverline] = UI::DockTabDimmedSelectedOverlineColor(); - colors[ImGuiCol_DockingPreview] = ImVec4(0.58f, 0.58f, 0.58f, 0.22f); - colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.18f, 0.18f, 0.18f, 1.00f); - colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); - colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.76f, 0.76f, 0.76f, 1.00f); - colors[ImGuiCol_PlotHistogram] = ImVec4(0.56f, 0.56f, 0.56f, 1.00f); - colors[ImGuiCol_PlotHistogramHovered] = ImVec4(0.66f, 0.66f, 0.66f, 1.00f); - colors[ImGuiCol_TableHeaderBg] = ImVec4(0.21f, 0.21f, 0.21f, 1.00f); - colors[ImGuiCol_TableBorderStrong] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); - colors[ImGuiCol_TableBorderLight] = ImVec4(0.18f, 0.18f, 0.18f, 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.03f); - colors[ImGuiCol_TextSelectedBg] = ImVec4(0.48f, 0.48f, 0.48f, 0.28f); - colors[ImGuiCol_DragDropTarget] = ImVec4(0.62f, 0.62f, 0.62f, 0.72f); - colors[ImGuiCol_NavHighlight] = ImVec4(0.62f, 0.62f, 0.62f, 0.52f); - 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 = 0.0f; - style.ChildRounding = 0.0f; - style.FrameRounding = 2.0f; - style.GrabRounding = 2.0f; - style.PopupRounding = 0.0f; - style.ScrollbarRounding = 2.0f; - style.TabRounding = 0.0f; - style.WindowBorderSize = 1.0f; - style.ChildBorderSize = 0.0f; - style.PopupBorderSize = 1.0f; - style.FrameBorderSize = 1.0f; - style.TabBorderSize = 0.0f; - style.TabBarBorderSize = 1.0f; - style.TabBarOverlineSize = 2.0f; - style.WindowPadding = ImVec2(6.0f, 6.0f); - style.FramePadding = ImVec2(6.0f, 4.0f); - style.ItemSpacing = ImVec2(6.0f, 4.0f); - style.ItemInnerSpacing = ImVec2(5.0f, 4.0f); - style.CellPadding = ImVec2(5.0f, 4.0f); - style.IndentSpacing = 14.0f; - style.ScrollbarSize = 14.0f; - style.GrabMinSize = 10.0f; - style.WindowTitleAlign = ImVec2(0.0f, 0.5f); - style.ButtonTextAlign = ImVec2(0.5f, 0.5f); - style.SelectableTextAlign = ImVec2(0.0f, 0.5f); + UI::ApplyBaseTheme(ImGui::GetStyle()); } } diff --git a/editor/src/UI/BaseTheme.h b/editor/src/UI/BaseTheme.h new file mode 100644 index 00000000..f3f67f73 --- /dev/null +++ b/editor/src/UI/BaseTheme.h @@ -0,0 +1,106 @@ +#pragma once + +#include "StyleTokens.h" + +#include + +namespace XCEngine { +namespace Editor { +namespace UI { + +inline void ApplyBaseThemeColors(ImVec4* colors) { + colors[ImGuiCol_Text] = ImVec4(0.80f, 0.80f, 0.80f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.53f, 0.53f, 0.53f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.22f, 0.22f, 0.22f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.22f, 0.22f, 0.22f, 1.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.17f, 0.17f, 0.17f, 0.98f); + colors[ImGuiCol_Border] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.18f, 0.18f, 0.18f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.21f, 0.21f, 0.21f, 1.00f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); + colors[ImGuiCol_TitleBg] = ImVec4(0.18f, 0.18f, 0.18f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.18f, 0.18f, 0.18f, 1.00f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.19f, 0.19f, 0.19f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.16f, 0.16f, 0.16f, 1.00f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.36f, 0.36f, 0.36f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.41f, 0.41f, 0.41f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.72f, 0.72f, 0.72f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.44f, 0.44f, 0.44f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.54f, 0.54f, 0.54f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.28f, 0.28f, 0.28f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.31f, 0.31f, 0.31f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.27f, 0.27f, 0.27f, 1.00f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.13f, 0.13f, 0.13f, 1.00f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.34f, 0.34f, 0.34f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.24f, 0.24f, 0.24f, 0.00f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.36f, 0.36f, 0.36f, 0.25f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.52f, 0.52f, 0.52f, 0.25f); + colors[ImGuiCol_Tab] = DockTabColor(); + colors[ImGuiCol_TabHovered] = DockTabHoveredColor(); + colors[ImGuiCol_TabSelected] = DockTabSelectedColor(); + colors[ImGuiCol_TabSelectedOverline] = DockTabSelectedOverlineColor(); + colors[ImGuiCol_TabDimmed] = DockTabDimmedColor(); + colors[ImGuiCol_TabDimmedSelected] = DockTabDimmedSelectedColor(); + colors[ImGuiCol_TabDimmedSelectedOverline] = DockTabDimmedSelectedOverlineColor(); + colors[ImGuiCol_DockingPreview] = ImVec4(0.58f, 0.58f, 0.58f, 0.22f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.18f, 0.18f, 0.18f, 1.00f); + colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.76f, 0.76f, 0.76f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.56f, 0.56f, 0.56f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(0.66f, 0.66f, 0.66f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.21f, 0.21f, 0.21f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + colors[ImGuiCol_TableBorderLight] = ImVec4(0.18f, 0.18f, 0.18f, 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.03f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.48f, 0.48f, 0.48f, 0.28f); + colors[ImGuiCol_DragDropTarget] = ImVec4(0.62f, 0.62f, 0.62f, 0.72f); + colors[ImGuiCol_NavHighlight] = ImVec4(0.62f, 0.62f, 0.62f, 0.52f); + 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); +} + +inline void ApplyBaseThemeMetrics(ImGuiStyle& style) { + style.WindowRounding = 0.0f; + style.ChildRounding = 0.0f; + style.FrameRounding = 2.0f; + style.GrabRounding = 2.0f; + style.PopupRounding = 0.0f; + style.ScrollbarRounding = 2.0f; + style.TabRounding = 0.0f; + style.WindowBorderSize = 1.0f; + style.ChildBorderSize = 0.0f; + style.PopupBorderSize = 1.0f; + style.FrameBorderSize = 1.0f; + style.TabBorderSize = 0.0f; + style.TabBarBorderSize = 1.0f; + style.TabBarOverlineSize = 2.0f; + style.WindowPadding = ImVec2(6.0f, 6.0f); + style.FramePadding = ImVec2(6.0f, 4.0f); + style.ItemSpacing = ImVec2(6.0f, 4.0f); + style.ItemInnerSpacing = ImVec2(5.0f, 4.0f); + style.CellPadding = ImVec2(5.0f, 4.0f); + style.IndentSpacing = 14.0f; + style.ScrollbarSize = 14.0f; + style.GrabMinSize = 10.0f; + style.WindowTitleAlign = ImVec2(0.0f, 0.5f); + style.ButtonTextAlign = ImVec2(0.5f, 0.5f); + style.SelectableTextAlign = ImVec2(0.0f, 0.5f); +} + +inline void ApplyBaseTheme(ImGuiStyle& style) { + ApplyBaseThemeColors(style.Colors); + ApplyBaseThemeMetrics(style); +} + +} // namespace UI +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/UI/Core.h b/editor/src/UI/Core.h index cb28f405..8ebed7a3 100644 --- a/editor/src/UI/Core.h +++ b/editor/src/UI/Core.h @@ -9,15 +9,15 @@ namespace Editor { namespace UI { inline float DefaultControlLabelWidth() { - return 104.0f; + return InspectorPropertyLabelWidth(); } inline ImVec2 DefaultControlCellPadding() { - return ImVec2(0.0f, 2.0f); + return ControlCellPadding(); } inline ImVec2 DefaultControlFramePadding() { - return ImVec2(6.0f, 3.0f); + return ControlFramePadding(); } inline void PushControlRowStyles() { @@ -25,6 +25,37 @@ inline void PushControlRowStyles() { ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, DefaultControlFramePadding()); } +template +inline auto DrawControlRow( + const char* label, + float columnWidth, + DrawControlFn&& drawControl) -> decltype(drawControl()) { + using Result = decltype(drawControl()); + + Result result{}; + ImGui::PushID(label); + PushControlRowStyles(); + + if (ImGui::BeginTable("##ControlRow", 2, ImGuiTableFlags_NoSavedSettings)) { + ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); + ImGui::TableSetupColumn("##control", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + ControlRowHeightOffset()); + + ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(label); + + ImGui::TableNextColumn(); + result = drawControl(); + + ImGui::EndTable(); + } + + ImGui::PopStyleVar(2); + ImGui::PopID(); + return result; +} + inline void StyleVarPush(ImGuiStyleVar idx, float val) { ImGui::PushStyleVar(idx, val); } @@ -49,8 +80,12 @@ inline void PopStyleColor(int count = 1) { ImGui::PopStyleColor(count); } +inline void PushPopupWindowStyle() { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, PopupWindowPadding()); +} + inline bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0) { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 10.0f)); + PushPopupWindowStyle(); bool is_open = ImGui::BeginPopup(str_id, flags); if (!is_open) { ImGui::PopStyleVar(); @@ -58,6 +93,36 @@ inline bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0) { return is_open; } +inline bool BeginPopupContextItem(const char* str_id = nullptr, ImGuiPopupFlags popup_flags = ImGuiPopupFlags_MouseButtonRight) { + PushPopupWindowStyle(); + bool is_open = ImGui::BeginPopupContextItem(str_id, popup_flags); + if (!is_open) { + ImGui::PopStyleVar(); + } + return is_open; +} + +inline bool BeginPopupContextWindow(const char* str_id = nullptr, ImGuiPopupFlags popup_flags = ImGuiPopupFlags_MouseButtonRight) { + PushPopupWindowStyle(); + bool is_open = ImGui::BeginPopupContextWindow(str_id, popup_flags); + if (!is_open) { + ImGui::PopStyleVar(); + } + return is_open; +} + +inline bool BeginModalPopup( + const char* name, + bool* p_open = nullptr, + ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize) { + PushPopupWindowStyle(); + bool is_open = ImGui::BeginPopupModal(name, p_open, flags); + if (!is_open) { + ImGui::PopStyleVar(); + } + return is_open; +} + inline void EndPopup() { ImGui::EndPopup(); ImGui::PopStyleVar(); diff --git a/editor/src/UI/PopupState.h b/editor/src/UI/PopupState.h new file mode 100644 index 00000000..0052f1b6 --- /dev/null +++ b/editor/src/UI/PopupState.h @@ -0,0 +1,82 @@ +#pragma once + +#include + +#include + +namespace XCEngine { +namespace Editor { +namespace UI { + +class DeferredPopupState { +public: + void RequestOpen() { + m_openRequested = true; + } + + void ConsumeOpenRequest(const char* popupId) { + if (!m_openRequested) { + return; + } + + ImGui::OpenPopup(popupId); + m_openRequested = false; + } + + bool HasPendingOpenRequest() const { + return m_openRequested; + } + +private: + bool m_openRequested = false; +}; + +template +class TextInputPopupState { +public: + void RequestOpen(const char* initialValue = "") { + SetValue(initialValue); + m_popup.RequestOpen(); + } + + void ConsumeOpenRequest(const char* popupId) { + m_popup.ConsumeOpenRequest(popupId); + } + + void SetValue(const char* value) { + if (value && value[0] != '\0') { + strcpy_s(m_buffer, value); + return; + } + + m_buffer[0] = '\0'; + } + + void Clear() { + m_buffer[0] = '\0'; + } + + bool Empty() const { + return m_buffer[0] == '\0'; + } + + char* Buffer() { + return m_buffer; + } + + const char* Buffer() const { + return m_buffer; + } + + constexpr size_t BufferSize() const { + return BufferCapacity; + } + +private: + DeferredPopupState m_popup; + char m_buffer[BufferCapacity] = {}; +}; + +} // namespace UI +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/UI/PropertyGrid.h b/editor/src/UI/PropertyGrid.h new file mode 100644 index 00000000..77c47485 --- /dev/null +++ b/editor/src/UI/PropertyGrid.h @@ -0,0 +1,131 @@ +#pragma once + +#include "Core/IUndoManager.h" +#include "ScalarControls.h" +#include "StyleTokens.h" +#include "VectorControls.h" + +namespace XCEngine { +namespace Editor { +namespace UI { + +template +inline bool ApplyPropertyChange( + bool changedByWidget, + ::XCEngine::Editor::IUndoManager* undoManager, + const char* undoLabel, + ApplyFn&& apply) { + if (!changedByWidget) { + return false; + } + + if (undoManager) { + undoManager->BeginInteractiveChange(undoLabel); + } + + apply(); + return true; +} + +inline bool DrawPropertyFloat( + const char* label, + float& value, + float dragSpeed = 0.1f, + float min = 0.0f, + float max = 0.0f, + const char* format = "%.2f" +) { + return DrawFloat(label, value, InspectorPropertyLabelWidth(), dragSpeed, min, max, format); +} + +inline bool DrawPropertyInt( + const char* label, + int& value, + int step = 1, + int min = 0, + int max = 0 +) { + return DrawInt(label, value, InspectorPropertyLabelWidth(), step, min, max); +} + +inline bool DrawPropertyBool( + const char* label, + bool& value +) { + return DrawBool(label, value, InspectorPropertyLabelWidth()); +} + +inline bool DrawPropertyColor3( + const char* label, + float color[3] +) { + return DrawColor3(label, color, InspectorPropertyLabelWidth()); +} + +inline bool DrawPropertyColor4( + const char* label, + float color[4] +) { + return DrawColor4(label, color, InspectorPropertyLabelWidth()); +} + +inline bool DrawPropertySliderFloat( + const char* label, + float& value, + float min, + float max, + const char* format = "%.2f" +) { + return DrawSliderFloat(label, value, min, max, InspectorPropertyLabelWidth(), format); +} + +inline bool DrawPropertySliderInt( + const char* label, + int& value, + int min, + int max +) { + return DrawSliderInt(label, value, min, max, InspectorPropertyLabelWidth()); +} + +inline int DrawPropertyCombo( + const char* label, + int currentItem, + const char* const items[], + int itemCount, + int heightInItems = -1 +) { + return DrawCombo(label, currentItem, items, itemCount, InspectorPropertyLabelWidth(), heightInItems); +} + +inline bool DrawPropertyVec2( + const char* label, + ::XCEngine::Math::Vector2& values, + float resetValue = 0.0f, + float dragSpeed = 0.1f +) { + return DrawVec2(label, values, resetValue, InspectorPropertyLabelWidth(), dragSpeed); +} + +inline bool DrawPropertyVec3( + const char* label, + ::XCEngine::Math::Vector3& values, + float resetValue = 0.0f, + float dragSpeed = 0.1f, + bool* isActive = nullptr +) { + return DrawVec3(label, values, resetValue, InspectorPropertyLabelWidth(), dragSpeed, isActive); +} + +inline bool DrawPropertyVec3Input( + const char* label, + ::XCEngine::Math::Vector3& values, + float dragSpeed = 0.1f, + bool* isActive = nullptr +) { + return DrawVec3Input(label, values, InspectorPropertyLabelWidth(), dragSpeed, isActive); +} + +} +} +} diff --git a/editor/src/UI/ScalarControls.h b/editor/src/UI/ScalarControls.h index 807bada0..cf4eb16f 100644 --- a/editor/src/UI/ScalarControls.h +++ b/editor/src/UI/ScalarControls.h @@ -1,5 +1,6 @@ #pragma once +#include "Core.h" #include namespace XCEngine { @@ -9,194 +10,58 @@ namespace UI { inline bool DrawFloat( const char* label, float& value, - float columnWidth = 100.0f, + float columnWidth = DefaultControlLabelWidth(), float dragSpeed = 0.1f, float min = 0.0f, float max = 0.0f, const char* format = "%.2f" ) { - bool changed = false; - ImGui::PushID(label); - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2{0, 1}); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 1}); - - if (ImGui::BeginTable("##FloatTable", 2, ImGuiTableFlags_NoSavedSettings)) { - ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); - ImGui::TableSetupColumn("##control", ImGuiTableColumnFlags_WidthStretch); - - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + 2.0f); - - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::Text(label); - - ImGui::TableNextColumn(); - - if (max != min) { - if (ImGui::SliderFloat("##value", &value, min, max, format)) { - changed = true; - } - } else { - if (ImGui::DragFloat("##value", &value, dragSpeed, min, max, format)) { - changed = true; - } - } - - ImGui::EndTable(); - } - - ImGui::PopStyleVar(2); - ImGui::PopID(); - - return changed; + return DrawControlRow(label, columnWidth, [&]() { + return ImGui::DragFloat("##value", &value, dragSpeed, min, max, format); + }); } inline bool DrawInt( const char* label, int& value, - float columnWidth = 100.0f, + float columnWidth = DefaultControlLabelWidth(), int step = 1, int min = 0, int max = 0 ) { - bool changed = false; - ImGui::PushID(label); - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2{0, 1}); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 1}); - - if (ImGui::BeginTable("##IntTable", 2, ImGuiTableFlags_NoSavedSettings)) { - ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); - ImGui::TableSetupColumn("##control", ImGuiTableColumnFlags_WidthStretch); - - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + 2.0f); - - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::Text(label); - - ImGui::TableNextColumn(); - - if (ImGui::DragInt("##value", &value, static_cast(step), min, max)) { - changed = true; - } - - ImGui::EndTable(); - } - - ImGui::PopStyleVar(2); - ImGui::PopID(); - - return changed; + return DrawControlRow(label, columnWidth, [&]() { + return ImGui::DragInt("##value", &value, static_cast(step), min, max); + }); } inline bool DrawBool( const char* label, bool& value, - float columnWidth = 100.0f + float columnWidth = DefaultControlLabelWidth() ) { - bool changed = false; - ImGui::PushID(label); - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2{0, 1}); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 1}); - - if (ImGui::BeginTable("##BoolTable", 2, ImGuiTableFlags_NoSavedSettings)) { - ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); - ImGui::TableSetupColumn("##control", ImGuiTableColumnFlags_WidthStretch); - - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + 2.0f); - - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::Text(label); - - ImGui::TableNextColumn(); - - if (ImGui::Checkbox("##value", &value)) { - changed = true; - } - - ImGui::EndTable(); - } - - ImGui::PopStyleVar(2); - ImGui::PopID(); - - return changed; + return DrawControlRow(label, columnWidth, [&]() { + return ImGui::Checkbox("##value", &value); + }); } inline bool DrawColor3( const char* label, float color[3], - float columnWidth = 100.0f + float columnWidth = DefaultControlLabelWidth() ) { - bool changed = false; - ImGui::PushID(label); - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2{0, 1}); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 1}); - - if (ImGui::BeginTable("##Color3Table", 2, ImGuiTableFlags_NoSavedSettings)) { - ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); - ImGui::TableSetupColumn("##control", ImGuiTableColumnFlags_WidthStretch); - - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + 2.0f); - - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::Text(label); - - ImGui::TableNextColumn(); - - if (ImGui::ColorEdit3("##value", color, ImGuiColorEditFlags_NoInputs)) { - changed = true; - } - - ImGui::EndTable(); - } - - ImGui::PopStyleVar(2); - ImGui::PopID(); - - return changed; + return DrawControlRow(label, columnWidth, [&]() { + return ImGui::ColorEdit3("##value", color, ImGuiColorEditFlags_NoInputs); + }); } inline bool DrawColor4( const char* label, float color[4], - float columnWidth = 100.0f + float columnWidth = DefaultControlLabelWidth() ) { - bool changed = false; - ImGui::PushID(label); - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2{0, 1}); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 1}); - - if (ImGui::BeginTable("##Color4Table", 2, ImGuiTableFlags_NoSavedSettings)) { - ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); - ImGui::TableSetupColumn("##control", ImGuiTableColumnFlags_WidthStretch); - - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + 2.0f); - - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::Text(label); - - ImGui::TableNextColumn(); - - if (ImGui::ColorEdit4("##value", color, ImGuiColorEditFlags_NoInputs)) { - changed = true; - } - - ImGui::EndTable(); - } - - ImGui::PopStyleVar(2); - ImGui::PopID(); - - return changed; + return DrawControlRow(label, columnWidth, [&]() { + return ImGui::ColorEdit4("##value", color, ImGuiColorEditFlags_NoInputs); + }); } inline bool DrawSliderFloat( @@ -204,38 +69,12 @@ inline bool DrawSliderFloat( float& value, float min, float max, - float columnWidth = 100.0f, + float columnWidth = DefaultControlLabelWidth(), const char* format = "%.2f" ) { - bool changed = false; - ImGui::PushID(label); - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2{0, 1}); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 1}); - - if (ImGui::BeginTable("##SliderTable", 2, ImGuiTableFlags_NoSavedSettings)) { - ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); - ImGui::TableSetupColumn("##control", ImGuiTableColumnFlags_WidthStretch); - - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + 2.0f); - - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::Text(label); - - ImGui::TableNextColumn(); - - if (ImGui::SliderFloat("##value", &value, min, max, format)) { - changed = true; - } - - ImGui::EndTable(); - } - - ImGui::PopStyleVar(2); - ImGui::PopID(); - - return changed; + return DrawControlRow(label, columnWidth, [&]() { + return ImGui::SliderFloat("##value", &value, min, max, format); + }); } inline bool DrawSliderInt( @@ -243,37 +82,11 @@ inline bool DrawSliderInt( int& value, int min, int max, - float columnWidth = 100.0f + float columnWidth = DefaultControlLabelWidth() ) { - bool changed = false; - ImGui::PushID(label); - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2{0, 1}); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 1}); - - if (ImGui::BeginTable("##SliderIntTable", 2, ImGuiTableFlags_NoSavedSettings)) { - ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); - ImGui::TableSetupColumn("##control", ImGuiTableColumnFlags_WidthStretch); - - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + 2.0f); - - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::Text(label); - - ImGui::TableNextColumn(); - - if (ImGui::SliderInt("##value", &value, min, max)) { - changed = true; - } - - ImGui::EndTable(); - } - - ImGui::PopStyleVar(2); - ImGui::PopID(); - - return changed; + return DrawControlRow(label, columnWidth, [&]() { + return ImGui::SliderInt("##value", &value, min, max); + }); } inline int DrawCombo( @@ -281,39 +94,17 @@ inline int DrawCombo( int currentItem, const char* const items[], int itemCount, - float columnWidth = 100.0f, + float columnWidth = DefaultControlLabelWidth(), int heightInItems = -1 ) { - ImGui::PushID(label); - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2{0, 1}); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 1}); - int changedItem = currentItem; - - if (ImGui::BeginTable("##ComboTable", 2, ImGuiTableFlags_NoSavedSettings)) { - ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); - ImGui::TableSetupColumn("##control", ImGuiTableColumnFlags_WidthStretch); - - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + 2.0f); - - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::Text(label); - - ImGui::TableNextColumn(); - + DrawControlRow(label, columnWidth, [&]() { ImGui::SetNextItemWidth(-1); if (ImGui::Combo("##value", ¤tItem, items, itemCount, heightInItems)) { changedItem = currentItem; } - - ImGui::EndTable(); - } - - ImGui::PopStyleVar(2); - ImGui::PopID(); - + return false; + }); return changedItem; } diff --git a/editor/src/UI/StyleTokens.h b/editor/src/UI/StyleTokens.h index 4c2bbceb..23b7848a 100644 --- a/editor/src/UI/StyleTokens.h +++ b/editor/src/UI/StyleTokens.h @@ -66,10 +66,46 @@ inline ImVec2 ToolbarItemSpacing() { return ImVec2(6.0f, 6.0f); } +inline float StandardPanelToolbarHeight() { + return 34.0f; +} + +inline float ProjectPanelToolbarHeight() { + return 60.0f; +} + +inline float ToolbarSearchTrailingSpacing() { + return 4.0f; +} + +inline float ToolbarRowGap() { + return 2.0f; +} + +inline ImVec2 AssetGridSpacing() { + return ImVec2(8.0f, 10.0f); +} + inline ImVec2 DefaultPanelContentPadding() { return ImVec2(8.0f, 6.0f); } +inline float InspectorPropertyLabelWidth() { + return 104.0f; +} + +inline ImVec2 ControlCellPadding() { + return ImVec2(0.0f, 2.0f); +} + +inline ImVec2 ControlFramePadding() { + return ImVec2(6.0f, 3.0f); +} + +inline float ControlRowHeightOffset() { + return 2.0f; +} + inline ImVec2 InspectorPanelContentPadding() { return ImVec2(10.0f, 0.0f); } @@ -82,10 +118,162 @@ inline ImVec4 ToolbarBackgroundColor() { return ImVec4(0.19f, 0.19f, 0.19f, 1.0f); } +inline ImVec2 SearchFieldFramePadding() { + return ImVec2(7.0f, 4.0f); +} + +inline float SearchFieldFrameRounding() { + return 2.0f; +} + inline ImU32 PanelDividerColor() { return IM_COL32(36, 36, 36, 255); } +inline ImVec4 EmptyStateSubtitleColor() { + return ImVec4(0.55f, 0.55f, 0.55f, 1.0f); +} + +inline ImVec4 HintTextColor() { + return ImVec4(0.53f, 0.53f, 0.53f, 1.0f); +} + +inline float EmptyStateLineOffset() { + return 20.0f; +} + +inline ImVec2 InspectorSectionFramePadding() { + return ImVec2(6.0f, 4.0f); +} + +inline ImVec2 InspectorSectionItemSpacing() { + return ImVec2(6.0f, 0.0f); +} + +inline ImVec2 InspectorActionButtonPadding() { + return ImVec2(8.0f, 5.0f); +} + +inline ImVec2 PopupWindowPadding() { + return ImVec2(12.0f, 10.0f); +} + +inline ImVec2 HierarchyNodeFramePadding() { + return ImVec2(4.0f, 3.0f); +} + +inline ImVec2 AssetTileSize() { + return ImVec2(104.0f, 82.0f); +} + +inline float HierarchyOverflowButtonWidth() { + return 26.0f; +} + +inline ImVec2 ProjectBackButtonSize() { + return ImVec2(28.0f, 0.0f); +} + +inline ImVec2 DialogActionButtonSize() { + return ImVec2(80.0f, 0.0f); +} + +inline ImVec2 AssetDragPreviewSize() { + return ImVec2(24.0f, 20.0f); +} + +inline float AssetTileRounding() { + return 2.0f; +} + +inline ImVec4 AssetTileHoverFillColor() { + return ImVec4(1.0f, 1.0f, 1.0f, 0.04f); +} + +inline ImVec4 AssetTileSelectedFillColor() { + return ImVec4(0.61f, 0.61f, 0.61f, 0.12f); +} + +inline ImVec4 AssetTileSelectedBorderColor() { + return ImVec4(0.74f, 0.74f, 0.74f, 0.43f); +} + +inline ImVec4 AssetTileDraggedOverlayColor() { + return ImVec4(0.0f, 0.0f, 0.0f, 0.24f); +} + +inline ImVec2 AssetTileIconOffset() { + return ImVec2(14.0f, 12.0f); +} + +inline ImVec2 AssetTileIconSize() { + return ImVec2(28.0f, 22.0f); +} + +inline ImVec2 AssetTileTextPadding() { + return ImVec2(6.0f, 10.0f); +} + +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.46f, 0.46f, 0.46f, 1.0f); +} + +inline ImVec4 AssetFolderIconLineColor() { + return ImVec4(0.72f, 0.72f, 0.72f, 0.86f); +} + +inline ImVec4 AssetFileIconFillColor() { + return ImVec4(0.41f, 0.41f, 0.41f, 1.0f); +} + +inline ImVec4 AssetFileIconLineColor() { + return ImVec4(0.65f, 0.65f, 0.65f, 0.86f); +} + +inline ImVec4 AssetFileFoldColor() { + return ImVec4(0.92f, 0.92f, 0.92f, 0.16f); +} + +inline ImVec4 ConsoleRowHoverFillColor() { + return ImVec4(1.0f, 1.0f, 1.0f, 0.03f); +} + +inline float MenuBarStatusRightPadding() { + return 20.0f; +} + +inline ImVec4 MenuBarStatusIdleColor() { + return ImVec4(0.62f, 0.62f, 0.62f, 1.0f); +} + +inline ImVec4 MenuBarStatusDirtyColor() { + return ImVec4(0.82f, 0.82f, 0.82f, 1.0f); +} + +inline ImVec2 VectorAxisInputSpacing() { + return ImVec2(6.0f, 0.0f); +} + +inline ImVec2 VectorAxisControlSpacing() { + return ImVec2(0.0f, 0.0f); +} + +inline float VectorAxisResetButtonExtraWidth() { + return 3.0f; +} + +inline ImVec4 VectorAxisButtonColor() { + return ImVec4(0.28f, 0.28f, 0.28f, 1.0f); +} + +inline ImVec4 VectorAxisButtonHoveredColor() { + return ImVec4(0.34f, 0.34f, 0.34f, 1.0f); +} + inline ImVec4 ToolbarButtonColor(bool active) { return active ? ImVec4(0.33f, 0.33f, 0.33f, 1.0f) : ImVec4(0.24f, 0.24f, 0.24f, 1.0f); } diff --git a/editor/src/UI/UI.h b/editor/src/UI/UI.h index 23ec2017..47a845a7 100644 --- a/editor/src/UI/UI.h +++ b/editor/src/UI/UI.h @@ -1,11 +1,15 @@ #pragma once +#include "BaseTheme.h" #include "Core.h" #include "DockHostStyle.h" #include "PanelChrome.h" +#include "PopupState.h" +#include "PropertyGrid.h" +#include "ScalarControls.h" #include "StyleTokens.h" #include "VectorControls.h" -#include "ScalarControls.h" +#include "Widgets.h" namespace XCEngine { namespace Editor { diff --git a/editor/src/UI/VectorControls.h b/editor/src/UI/VectorControls.h index 8aa4e760..19643db0 100644 --- a/editor/src/UI/VectorControls.h +++ b/editor/src/UI/VectorControls.h @@ -1,148 +1,155 @@ #pragma once +#include "Core.h" #include #include #include +#include namespace XCEngine { namespace Editor { namespace UI { -inline bool DrawVec3( - const char* label, - ::XCEngine::Math::Vector3& values, - float resetValue = 0.0f, - float columnWidth = 100.0f, - float dragSpeed = 0.1f +struct AxisFloatControlSpec { + const char* label = nullptr; + float* value = nullptr; +}; + +inline bool DrawAxisFloatControls( + const AxisFloatControlSpec* axes, + int axisCount, + float dragSpeed, + bool useResetButtons, + float resetValue, + bool* isActive = nullptr ) { bool changed = false; - ImGui::PushID(label); + bool anyItemActive = false; - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2{0, 1}); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 1}); + ImGui::PushStyleVar( + ImGuiStyleVar_ItemSpacing, + useResetButtons ? VectorAxisControlSpacing() : VectorAxisInputSpacing()); - if (ImGui::BeginTable("##Vec3Table", 2, ImGuiTableFlags_NoSavedSettings)) { - ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); - ImGui::TableSetupColumn("##controls", ImGuiTableColumnFlags_WidthStretch); + const float availableWidth = ImGui::GetContentRegionAvail().x; + const float spacing = ImGui::GetStyle().ItemSpacing.x; - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + 2.0f); + if (useResetButtons) { + const float lineHeight = ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f; + const ImVec2 buttonSize(lineHeight + VectorAxisResetButtonExtraWidth(), lineHeight); + float itemWidth = (availableWidth - buttonSize.x * static_cast(axisCount)) / static_cast(axisCount); + if (itemWidth < 0.0f) { + itemWidth = 0.0f; + } - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::Text(label); + const ImVec4 buttonColor = VectorAxisButtonColor(); + const ImVec4 buttonHoverColor = VectorAxisButtonHoveredColor(); - ImGui::TableNextColumn(); + for (int i = 0; i < axisCount; ++i) { + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonHoverColor); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, buttonColor); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{0, 0}); - - float lineHeight = ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f; - ImVec2 buttonSize = {lineHeight + 3.0f, lineHeight}; - float itemWidth = (ImGui::GetContentRegionAvail().x - buttonSize.x * 3.0f) / 3.0f; - - auto drawAxisControl = [&](const char* axisLabel, float& value, ImVec4 color, ImVec4 colorHovered) { - bool axisChanged = false; - - ImGui::PushStyleColor(ImGuiCol_Button, color); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colorHovered); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, color); - - if (ImGui::Button(axisLabel, buttonSize)) { - value = resetValue; - axisChanged = true; + if (ImGui::Button(axes[i].label, buttonSize)) { + *axes[i].value = resetValue; + changed = true; } ImGui::PopStyleColor(3); ImGui::SameLine(); ImGui::SetNextItemWidth(itemWidth); - - if (ImGui::DragFloat((std::string("##") + axisLabel).c_str(), &value, dragSpeed, 0.0f, 0.0f, "%.2f")) { - axisChanged = true; + if (ImGui::DragFloat((std::string("##") + axes[i].label).c_str(), axes[i].value, dragSpeed, 0.0f, 0.0f, "%.2f")) { + changed = true; } + + anyItemActive = anyItemActive || ImGui::IsItemActive(); + if (i + 1 < axisCount) { + ImGui::SameLine(); + } + } + } else { + const float axisLabelWidth = ImGui::CalcTextSize("Z").x; + float itemWidth = (availableWidth - axisLabelWidth * static_cast(axisCount) - spacing * static_cast(axisCount * 2 - 1)) / static_cast(axisCount); + if (itemWidth < 0.0f) { + itemWidth = 0.0f; + } + + for (int i = 0; i < axisCount; ++i) { + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", axes[i].label); ImGui::SameLine(); + ImGui::SetNextItemWidth(itemWidth); + if (ImGui::DragFloat((std::string("##") + axes[i].label).c_str(), axes[i].value, dragSpeed, 0.0f, 0.0f, "%.2f")) { + changed = true; + } - return axisChanged; - }; - - changed |= drawAxisControl("X", values.x, ImVec4{0.8f, 0.1f, 0.15f, 1.0f}, ImVec4{0.9f, 0.2f, 0.2f, 1.0f}); - changed |= drawAxisControl("Y", values.y, ImVec4{0.2f, 0.7f, 0.2f, 1.0f}, ImVec4{0.3f, 0.8f, 0.3f, 1.0f}); - changed |= drawAxisControl("Z", values.z, ImVec4{0.1f, 0.25f, 0.8f, 1.0f}, ImVec4{0.2f, 0.35f, 0.9f, 1.0f}); - - ImGui::PopStyleVar(); - ImGui::EndTable(); + anyItemActive = anyItemActive || ImGui::IsItemActive(); + if (i + 1 < axisCount) { + ImGui::SameLine(); + } + } } - ImGui::PopStyleVar(2); - ImGui::PopID(); + ImGui::PopStyleVar(); + + if (isActive) { + *isActive = anyItemActive; + } return changed; } +inline bool DrawVec3Input( + const char* label, + ::XCEngine::Math::Vector3& values, + float columnWidth = DefaultControlLabelWidth(), + float dragSpeed = 0.1f, + bool* isActive = nullptr +) { + const AxisFloatControlSpec axes[] = { + { "X", &values.x }, + { "Y", &values.y }, + { "Z", &values.z } + }; + + return DrawControlRow(label, columnWidth, [&]() { + return DrawAxisFloatControls(axes, 3, dragSpeed, false, 0.0f, isActive); + }); +} + +inline bool DrawVec3( + const char* label, + ::XCEngine::Math::Vector3& values, + float resetValue = 0.0f, + float columnWidth = DefaultControlLabelWidth(), + float dragSpeed = 0.1f, + bool* isActive = nullptr +) { + const AxisFloatControlSpec axes[] = { + { "X", &values.x }, + { "Y", &values.y }, + { "Z", &values.z } + }; + + return DrawControlRow(label, columnWidth, [&]() { + return DrawAxisFloatControls(axes, 3, dragSpeed, true, resetValue, isActive); + }); +} + inline bool DrawVec2( const char* label, ::XCEngine::Math::Vector2& values, float resetValue = 0.0f, - float columnWidth = 100.0f, + float columnWidth = DefaultControlLabelWidth(), float dragSpeed = 0.1f ) { - bool changed = false; - ImGui::PushID(label); + const AxisFloatControlSpec axes[] = { + { "X", &values.x }, + { "Y", &values.y } + }; - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2{0, 1}); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 1}); - - if (ImGui::BeginTable("##Vec2Table", 2, ImGuiTableFlags_NoSavedSettings)) { - ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, columnWidth); - ImGui::TableSetupColumn("##controls", ImGuiTableColumnFlags_WidthStretch); - - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetFontSize() + 2.0f); - - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::Text(label); - - ImGui::TableNextColumn(); - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{0, 0}); - - float lineHeight = ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f; - ImVec2 buttonSize = {lineHeight + 3.0f, lineHeight}; - float itemWidth = (ImGui::GetContentRegionAvail().x - buttonSize.x * 2.0f) / 2.0f; - - auto drawAxisControl = [&](const char* axisLabel, float& value, ImVec4 color, ImVec4 colorHovered) { - bool axisChanged = false; - - ImGui::PushStyleColor(ImGuiCol_Button, color); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colorHovered); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, color); - - if (ImGui::Button(axisLabel, buttonSize)) { - value = resetValue; - axisChanged = true; - } - ImGui::PopStyleColor(3); - - ImGui::SameLine(); - ImGui::SetNextItemWidth(itemWidth); - - if (ImGui::DragFloat((std::string("##") + axisLabel).c_str(), &value, dragSpeed, 0.0f, 0.0f, "%.2f")) { - axisChanged = true; - } - ImGui::SameLine(); - - return axisChanged; - }; - - changed |= drawAxisControl("X", values.x, ImVec4{0.8f, 0.1f, 0.15f, 1.0f}, ImVec4{0.9f, 0.2f, 0.2f, 1.0f}); - changed |= drawAxisControl("Y", values.y, ImVec4{0.2f, 0.7f, 0.2f, 1.0f}, ImVec4{0.3f, 0.8f, 0.3f, 1.0f}); - - ImGui::PopStyleVar(); - ImGui::EndTable(); - } - - ImGui::PopStyleVar(2); - ImGui::PopID(); - - return changed; + return DrawControlRow(label, columnWidth, [&]() { + return DrawAxisFloatControls(axes, 2, dragSpeed, true, resetValue); + }); } } diff --git a/editor/src/UI/Widgets.h b/editor/src/UI/Widgets.h new file mode 100644 index 00000000..8b18f1fc --- /dev/null +++ b/editor/src/UI/Widgets.h @@ -0,0 +1,403 @@ +#pragma once + +#include "StyleTokens.h" + +#include +#include + +namespace XCEngine { +namespace Editor { +namespace UI { + +struct ComponentSectionResult { + bool open = false; + bool removeRequested = false; +}; + +struct HierarchyNodeResult { + bool open = false; + bool clicked = false; + bool doubleClicked = false; +}; + +struct AssetTileResult { + bool clicked = false; + bool contextRequested = false; + bool openRequested = false; + bool hovered = false; + ImVec2 min = ImVec2(0.0f, 0.0f); + ImVec2 max = ImVec2(0.0f, 0.0f); +}; + +enum class AssetIconKind { + Folder, + File +}; + +enum class DialogActionResult { + None, + Primary, + Secondary +}; + +enum class MenuCommandKind { + Action, + Separator +}; + +struct MenuCommand { + MenuCommandKind kind = MenuCommandKind::Action; + const char* label = nullptr; + const char* shortcut = nullptr; + bool selected = false; + bool enabled = true; + + static MenuCommand Action( + const char* label, + const char* shortcut = nullptr, + bool selected = false, + bool enabled = true) { + return MenuCommand{ MenuCommandKind::Action, label, shortcut, selected, enabled }; + } + + static MenuCommand Separator() { + return MenuCommand{ MenuCommandKind::Separator, nullptr, nullptr, false, true }; + } +}; + +template +inline bool DrawMenuScope(const char* label, DrawContentFn&& drawContent) { + if (!ImGui::BeginMenu(label)) { + return false; + } + + drawContent(); + ImGui::EndMenu(); + return true; +} + +template +inline bool DrawMenuCommand(const MenuCommand& command, ExecuteFn&& execute) { + if (command.kind == MenuCommandKind::Separator) { + ImGui::Separator(); + return false; + } + + if (!ImGui::MenuItem(command.label, command.shortcut, command.selected, command.enabled)) { + return false; + } + + execute(); + return true; +} + +template +inline void DrawMenuCommands(const MenuCommand (&commands)[N], ExecuteFn&& execute) { + for (size_t i = 0; i < N; ++i) { + DrawMenuCommand(commands[i], [&]() { execute(i); }); + } +} + +inline bool ToolbarSearchField( + const char* id, + const char* hint, + char* buffer, + size_t bufferSize, + float trailingWidth = 0.0f) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, SearchFieldFramePadding()); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, SearchFieldFrameRounding()); + const float width = ImGui::GetContentRegionAvail().x - trailingWidth; + ImGui::SetNextItemWidth(width > 0.0f ? width : 0.0f); + const bool changed = ImGui::InputTextWithHint(id, hint, buffer, bufferSize); + ImGui::PopStyleVar(2); + return changed; +} + +inline void DrawToolbarLabel(const char* text) { + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(HintTextColor(), "%s", text); +} + +inline bool ToolbarToggleButton(const char* label, bool& active, ImVec2 size = ImVec2(0.0f, 0.0f)) { + if (!ToolbarButton(label, active, size)) { + return false; + } + + active = !active; + return true; +} + +inline void DrawToolbarRowGap() { + ImGui::Dummy(ImVec2(0.0f, ToolbarRowGap())); +} + +inline void DrawHintText(const char* text) { + ImGui::TextColored(HintTextColor(), "%s", text); +} + +inline void DrawEmptyState( + const char* title, + const char* subtitle = nullptr, + ImVec2 start = ImVec2(10.0f, 10.0f)) { + ImGui::SetCursorPos(start); + ImGui::TextUnformatted(title); + + if (subtitle && subtitle[0] != '\0') { + ImGui::SetCursorPos(ImVec2(start.x, start.y + EmptyStateLineOffset())); + ImGui::TextColored(EmptyStateSubtitleColor(), "%s", subtitle); + } +} + +template +inline void DrawToolbarBreadcrumbs( + const char* rootLabel, + size_t segmentCount, + GetNameFn&& getName, + NavigateFn&& navigateToSegment) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + + if (segmentCount == 0) { + ImGui::TextUnformatted(rootLabel); + ImGui::PopStyleColor(2); + return; + } + + for (size_t i = 0; i < segmentCount; ++i) { + if (i > 0) { + ImGui::SameLine(); + ImGui::TextDisabled("/"); + ImGui::SameLine(); + } + + 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()); + } + } + + ImGui::PopStyleColor(2); +} + +inline HierarchyNodeResult DrawHierarchyNode( + const void* id, + const char* label, + bool selected, + bool leaf) { + ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_SpanAvailWidth | + ImGuiTreeNodeFlags_FramePadding; + if (leaf) { + flags |= ImGuiTreeNodeFlags_Leaf; + } + if (selected) { + flags |= ImGuiTreeNodeFlags_Selected; + } + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, HierarchyNodeFramePadding()); + const bool open = ImGui::TreeNodeEx(id, flags, "%s", label); + ImGui::PopStyleVar(); + + return HierarchyNodeResult{ + open, + ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen(), + ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0) + }; +} + +inline void EndHierarchyNode() { + ImGui::TreePop(); +} + +template +inline AssetTileResult DrawAssetTile( + const char* label, + bool selected, + bool dimmed, + DrawIconFn&& drawIcon) { + const ImVec2 tileSize = AssetTileSize(); + ImGui::InvisibleButton("##AssetBtn", tileSize); + + const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); + const bool contextRequested = ImGui::IsItemClicked(ImGuiMouseButton_Right); + const bool openRequested = ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0); + const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + + const ImVec2 min = ImGui::GetItemRectMin(); + const ImVec2 max = ImVec2(min.x + tileSize.x, min.y + tileSize.y); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + if (hovered || selected) { + drawList->AddRectFilled(min, max, ImGui::GetColorU32(selected ? AssetTileSelectedFillColor() : AssetTileHoverFillColor()), AssetTileRounding()); + } + if (selected) { + 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); + drawIcon(drawList, iconMin, iconMax); + + const ImVec2 textSize = ImGui::CalcTextSize(label); + 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::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); +} + +inline ComponentSectionResult BeginComponentSection( + const void* id, + const char* label, + bool canRemove, + bool defaultOpen = true) { + const ImGuiStyle& style = ImGui::GetStyle(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, InspectorSectionFramePadding()); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x, InspectorSectionItemSpacing().y)); + + ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_Framed | + ImGuiTreeNodeFlags_SpanAvailWidth | + ImGuiTreeNodeFlags_FramePadding | + ImGuiTreeNodeFlags_AllowOverlap; + if (defaultOpen) { + flags |= ImGuiTreeNodeFlags_DefaultOpen; + } + + const bool open = ImGui::TreeNodeEx(id, flags, "%s", label); + ImGui::PopStyleVar(2); + + bool removeRequested = false; + if (BeginPopupContextItem("##ComponentSettings")) { + DrawMenuCommand(MenuCommand::Action("Remove Component", nullptr, false, canRemove), [&]() { + removeRequested = true; + }); + EndPopup(); + } + + return ComponentSectionResult{ open, removeRequested }; +} + +inline void EndComponentSection() { + ImGui::TreePop(); +} + +inline bool InspectorActionButton(const char* label, ImVec2 size = ImVec2(-1.0f, 0.0f)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, InspectorActionButtonPadding()); + const bool pressed = ImGui::Button(label, size); + ImGui::PopStyleVar(); + return pressed; +} + +inline bool BeginTitledPopup(const char* id, const char* title) { + const bool open = BeginPopup(id); + if (!open) { + return false; + } + + if (title && title[0] != '\0') { + ImGui::TextUnformatted(title); + ImGui::Separator(); + } + + return true; +} + +inline void EndTitledPopup() { + EndPopup(); +} + +inline DialogActionResult DrawDialogActionRow( + const char* primaryLabel, + const char* secondaryLabel, + bool primaryEnabled = true, + bool secondaryEnabled = true) { + DialogActionResult result = DialogActionResult::None; + + ImGui::BeginDisabled(!primaryEnabled); + if (ImGui::Button(primaryLabel, DialogActionButtonSize())) { + result = DialogActionResult::Primary; + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + + ImGui::BeginDisabled(!secondaryEnabled); + if (ImGui::Button(secondaryLabel, DialogActionButtonSize())) { + result = DialogActionResult::Secondary; + } + ImGui::EndDisabled(); + + return result; +} + +inline void DrawRightAlignedText(const char* text, const ImVec4& color, float rightPadding = MenuBarStatusRightPadding()) { + const ImVec2 textSize = ImGui::CalcTextSize(text); + const float targetX = ImGui::GetWindowWidth() - textSize.x - rightPadding; + if (targetX > ImGui::GetCursorPosX()) { + ImGui::SetCursorPosX(targetX); + } + ImGui::TextColored(color, "%s", text); +} + +inline void BeginTitledTooltip(const char* title) { + ImGui::BeginTooltip(); + if (title && title[0] != '\0') { + ImGui::TextUnformatted(title); + ImGui::Separator(); + } +} + +inline void EndTitledTooltip() { + ImGui::EndTooltip(); +} + +inline bool DrawConsoleLogRow(const char* text) { + ImGui::TextUnformatted(text); + + if (ImGui::IsItemHovered()) { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), ImGui::GetColorU32(ConsoleRowHoverFillColor())); + } + + return ImGui::IsItemClicked(); +} + +} +} +} diff --git a/editor/src/panels/ConsolePanel.cpp b/editor/src/panels/ConsolePanel.cpp index efafccbd..8d5436b2 100644 --- a/editor/src/panels/ConsolePanel.cpp +++ b/editor/src/panels/ConsolePanel.cpp @@ -1,7 +1,7 @@ +#include "Actions/EditorActions.h" #include "ConsolePanel.h" #include "Core/EditorConsoleSink.h" -#include "UI/Core.h" -#include "UI/PanelChrome.h" +#include "UI/UI.h" #include #include #include @@ -9,12 +9,6 @@ namespace XCEngine { namespace Editor { -namespace { - -constexpr float kConsoleToolbarHeight = 34.0f; - -} // namespace - ConsolePanel::ConsolePanel() : Panel("Console") { } @@ -27,26 +21,20 @@ void ConsolePanel::Render() { auto* sink = Debug::EditorConsoleSink::GetInstance(); { - UI::PanelToolbarScope toolbar("ConsoleToolbar", kConsoleToolbarHeight); + UI::PanelToolbarScope toolbar("ConsoleToolbar", UI::StandardPanelToolbarHeight()); if (toolbar.IsOpen()) { - if (UI::ToolbarButton("Clear")) { + if (Actions::DrawToolbarAction(Actions::MakeClearConsoleAction())) { sink->Clear(); } ImGui::SameLine(); - ImGui::TextDisabled("Filter"); + UI::DrawToolbarLabel("Filter"); ImGui::SameLine(); - if (UI::ToolbarButton("Info", m_showInfo)) { - m_showInfo = !m_showInfo; - } + Actions::DrawToolbarToggleAction(Actions::MakeConsoleInfoFilterAction(m_showInfo), m_showInfo); ImGui::SameLine(); - if (UI::ToolbarButton("Warn", m_showWarning)) { - m_showWarning = !m_showWarning; - } + Actions::DrawToolbarToggleAction(Actions::MakeConsoleWarningFilterAction(m_showWarning), m_showWarning); ImGui::SameLine(); - if (UI::ToolbarButton("Error", m_showError)) { - m_showError = !m_showError; - } + Actions::DrawToolbarToggleAction(Actions::MakeConsoleErrorFilterAction(m_showError), m_showError); } } @@ -99,9 +87,7 @@ void ConsolePanel::Render() { const char* category = ::XCEngine::Debug::LogCategoryToString(log.category); std::string fullMessage = std::string(prefix) + "[" + category + "] " + log.message.CStr(); - ImGui::TextUnformatted(fullMessage.c_str()); - - if (ImGui::IsItemClicked()) { + if (UI::DrawConsoleLogRow(fullMessage.c_str())) { ImGui::SetClipboardText(fullMessage.c_str()); } diff --git a/editor/src/panels/GameViewPanel.cpp b/editor/src/panels/GameViewPanel.cpp index 6be4236f..e4f92b3b 100644 --- a/editor/src/panels/GameViewPanel.cpp +++ b/editor/src/panels/GameViewPanel.cpp @@ -1,5 +1,5 @@ #include "GameViewPanel.h" -#include "UI/PanelChrome.h" +#include "UI/UI.h" #include namespace XCEngine { diff --git a/editor/src/panels/GameViewPanel.h b/editor/src/panels/GameViewPanel.h index c30e6079..1024ee7d 100644 --- a/editor/src/panels/GameViewPanel.h +++ b/editor/src/panels/GameViewPanel.h @@ -9,9 +9,6 @@ class GameViewPanel : public Panel { public: GameViewPanel(); void Render() override; - -private: - void RenderGameView(); }; } diff --git a/editor/src/panels/HierarchyPanel.cpp b/editor/src/panels/HierarchyPanel.cpp index 66bd0f82..4d04aacf 100644 --- a/editor/src/panels/HierarchyPanel.cpp +++ b/editor/src/panels/HierarchyPanel.cpp @@ -1,3 +1,5 @@ +#include "Actions/EditorActions.h" +#include "Commands/EntityCommands.h" #include "HierarchyPanel.h" #include "Core/IEditorContext.h" #include "Core/ISceneManager.h" @@ -5,25 +7,13 @@ #include "Core/IUndoManager.h" #include "Core/EditorEvents.h" #include "Core/EventBus.h" -#include "UI/Core.h" -#include "UI/PanelChrome.h" -#include "Utils/UndoUtils.h" -#include -#include -#include -#include +#include "UI/UI.h" #include #include namespace XCEngine { namespace Editor { -namespace { - -constexpr float kHierarchyToolbarHeight = 34.0f; - -} // namespace - HierarchyPanel::HierarchyPanel() : Panel("Hierarchy") { } @@ -76,9 +66,9 @@ void HierarchyPanel::Render() { } } - if (ImGui::BeginPopupContextWindow("HierarchyContextMenu", ImGuiPopupFlags_MouseButtonRight)) { + if (UI::BeginPopupContextWindow("HierarchyContextMenu", ImGuiPopupFlags_MouseButtonRight)) { RenderCreateMenu(nullptr); - ImGui::EndPopup(); + UI::EndPopup(); } ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1)); @@ -86,20 +76,7 @@ void HierarchyPanel::Render() { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_PTR")) { ::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data; if (sourceGameObject && sourceGameObject->GetParent() != nullptr) { - auto& sceneManager = m_context->GetSceneManager(); - UndoUtils::ExecuteSceneCommand(*m_context, "Reparent Entity", [&]() { - auto* srcTransform = sourceGameObject->GetTransform(); - Math::Vector3 worldPos = srcTransform->GetPosition(); - Math::Quaternion worldRot = srcTransform->GetRotation(); - Math::Vector3 worldScale = srcTransform->GetScale(); - - sceneManager.MoveEntity(sourceGameObject->GetID(), 0); - - srcTransform->SetPosition(worldPos); - srcTransform->SetRotation(worldRot); - srcTransform->SetScale(worldScale); - sceneManager.MarkSceneDirty(); - }); + Commands::ReparentEntityPreserveWorldTransform(*m_context, sourceGameObject, 0); } } ImGui::EndDragDropTarget(); @@ -108,36 +85,47 @@ void HierarchyPanel::Render() { } void HierarchyPanel::RenderSearchBar() { - UI::PanelToolbarScope toolbar("HierarchyToolbar", kHierarchyToolbarHeight); + UI::PanelToolbarScope toolbar("HierarchyToolbar", UI::StandardPanelToolbarHeight()); if (!toolbar.IsOpen()) { return; } - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(7.0f, 4.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 2.0f); - - const float buttonWidth = 26.0f; - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - buttonWidth - 4.0f); - ImGui::InputTextWithHint("##Search", "Search hierarchy", m_searchBuffer, sizeof(m_searchBuffer)); + const float buttonWidth = UI::HierarchyOverflowButtonWidth(); + UI::ToolbarSearchField( + "##Search", + "Search hierarchy", + m_searchBuffer, + sizeof(m_searchBuffer), + buttonWidth + UI::ToolbarSearchTrailingSpacing()); ImGui::SameLine(); if (UI::ToolbarButton("...", false, ImVec2(buttonWidth, 0.0f))) { ImGui::OpenPopup("HierarchyOptions"); } - if (ImGui::BeginPopup("HierarchyOptions")) { - if (ImGui::MenuItem("Sort By Name", nullptr, m_sortMode == SortMode::Name)) { - m_sortMode = SortMode::Name; - } - if (ImGui::MenuItem("Sort By Component Count", nullptr, m_sortMode == SortMode::ComponentCount)) { - m_sortMode = SortMode::ComponentCount; - } - if (ImGui::MenuItem("Transform First", nullptr, m_sortMode == SortMode::TransformFirst)) { - m_sortMode = SortMode::TransformFirst; - } - ImGui::EndPopup(); - } + if (UI::BeginPopup("HierarchyOptions")) { + const UI::MenuCommand commands[] = { + UI::MenuCommand::Action("Sort By Name", nullptr, m_sortMode == SortMode::Name), + UI::MenuCommand::Action("Sort By Component Count", nullptr, m_sortMode == SortMode::ComponentCount), + UI::MenuCommand::Action("Transform First", nullptr, m_sortMode == SortMode::TransformFirst) + }; - ImGui::PopStyleVar(2); + UI::DrawMenuCommands(commands, [&](size_t index) { + switch (index) { + case 0: + m_sortMode = SortMode::Name; + break; + case 1: + m_sortMode = SortMode::ComponentCount; + break; + case 2: + m_sortMode = SortMode::TransformFirst; + break; + default: + break; + } + }); + UI::EndPopup(); + } } void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject, const std::string& filter) { @@ -148,20 +136,7 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject } ImGui::PushID(static_cast(gameObject->GetID())); - - ImGuiTreeNodeFlags flags = - ImGuiTreeNodeFlags_OpenOnArrow | - ImGuiTreeNodeFlags_SpanAvailWidth | - ImGuiTreeNodeFlags_FramePadding; - - if (gameObject->GetChildCount() == 0) { - flags |= ImGuiTreeNodeFlags_Leaf; - } - - if (m_context->GetSelectionManager().IsSelected(gameObject->GetID())) { - flags |= ImGuiTreeNodeFlags_Selected; - } - + if (m_renaming && m_renamingEntity == gameObject) { if (m_renameJustStarted) { ImGui::SetKeyboardFocusHere(); @@ -170,30 +145,22 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject ImGui::SetNextItemWidth(-1); if (ImGui::InputText("##Rename", m_renameBuffer, sizeof(m_renameBuffer), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) { - if (strlen(m_renameBuffer) > 0) { - UndoUtils::ExecuteSceneCommand(*m_context, "Rename Entity", [&]() { - m_context->GetSceneManager().RenameEntity(gameObject->GetID(), m_renameBuffer); - }); - } - m_renaming = false; - m_renamingEntity = nullptr; + CommitRename(); } - - if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) { - if (strlen(m_renameBuffer) > 0) { - UndoUtils::ExecuteSceneCommand(*m_context, "Rename Entity", [&]() { - m_context->GetSceneManager().RenameEntity(gameObject->GetID(), m_renameBuffer); - }); - } - m_renaming = false; - m_renamingEntity = nullptr; + + if (ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape)) { + CancelRename(); + } else if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) { + CommitRename(); } } else { - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 3.0f)); - bool isOpen = ImGui::TreeNodeEx((void*)gameObject->GetUUID(), flags, "%s", gameObject->GetName().c_str()); - ImGui::PopStyleVar(); + const UI::HierarchyNodeResult node = UI::DrawHierarchyNode( + (void*)gameObject->GetUUID(), + gameObject->GetName().c_str(), + m_context->GetSelectionManager().IsSelected(gameObject->GetID()), + gameObject->GetChildCount() == 0); - if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) { + if (node.clicked) { ImGuiIO& io = ImGui::GetIO(); if (io.KeyCtrl) { if (!m_context->GetSelectionManager().IsSelected(gameObject->GetID())) { @@ -204,25 +171,22 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject } } - if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { - m_renaming = true; - m_renamingEntity = gameObject; - strcpy_s(m_renameBuffer, gameObject->GetName().c_str()); - m_renameJustStarted = true; + if (node.doubleClicked) { + BeginRename(gameObject); } HandleDragDrop(gameObject); - if (ImGui::BeginPopupContextItem("EntityContextMenu")) { + if (UI::BeginPopupContextItem("EntityContextMenu")) { RenderContextMenu(gameObject); - ImGui::EndPopup(); + UI::EndPopup(); } - if (isOpen) { + if (node.open) { for (size_t i = 0; i < gameObject->GetChildCount(); i++) { RenderEntity(gameObject->GetChild(i), filter); } - ImGui::TreePop(); + UI::EndHierarchyNode(); } } @@ -230,132 +194,87 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject } void HierarchyPanel::RenderContextMenu(::XCEngine::Components::GameObject* gameObject) { - auto& sceneManager = m_context->GetSceneManager(); - auto& selectionManager = m_context->GetSelectionManager(); - - if (ImGui::BeginMenu("Create")) { + if (UI::DrawMenuScope("Create", [&]() { RenderCreateMenu(gameObject); - ImGui::EndMenu(); - } - - if (gameObject != nullptr && ImGui::MenuItem("Create Child")) { - UndoUtils::ExecuteSceneCommand(*m_context, "Create Child", [&]() { - auto* child = sceneManager.CreateEntity("GameObject", gameObject); - selectionManager.SetSelectedEntity(child->GetID()); - }); - } - - ImGui::Separator(); - - if (gameObject != nullptr && gameObject->GetParent() != nullptr) { - if (ImGui::MenuItem("Detach from Parent")) { - UndoUtils::ExecuteSceneCommand(*m_context, "Reparent Entity", [&]() { - sceneManager.MoveEntity(gameObject->GetID(), 0); - }); - } - } - - if (ImGui::MenuItem("Rename", "F2")) { - if (gameObject) { - m_renaming = true; - m_renamingEntity = gameObject; - strcpy_s(m_renameBuffer, gameObject->GetName().c_str()); - m_renameJustStarted = true; - } - } - - if (ImGui::MenuItem("Delete", "Delete")) { - UndoUtils::ExecuteSceneCommand(*m_context, "Delete Entity", [&]() { - sceneManager.DeleteEntity(gameObject->GetID()); - }); - } - - ImGui::Separator(); - - if (ImGui::MenuItem("Copy", "Ctrl+C")) { - sceneManager.CopyEntity(gameObject->GetID()); - } - - if (ImGui::MenuItem("Paste", "Ctrl+V", false, sceneManager.HasClipboardData())) { - UndoUtils::ExecuteSceneCommand(*m_context, "Paste Entity", [&]() { - const uint64_t newId = sceneManager.PasteEntity(gameObject->GetID()); - if (newId != 0) { - selectionManager.SetSelectedEntity(newId); - } - }); - } - - if (ImGui::MenuItem("Duplicate", "Ctrl+D")) { - uint64_t newId = 0; - UndoUtils::ExecuteSceneCommand(*m_context, "Duplicate Entity", [&]() { - newId = sceneManager.DuplicateEntity(gameObject->GetID()); - if (newId != 0) { - selectionManager.SetSelectedEntity(newId); - } - }); - if (newId != 0) { - } + })) { } + + Actions::DrawMenuAction(Actions::MakeCreateChildEntityAction(gameObject), [&]() { + Commands::CreateEmptyEntity(*m_context, gameObject, "Create Child", "GameObject"); + }); + Actions::DrawMenuSeparator(); + Actions::DrawMenuAction(Actions::MakeDetachEntityAction(gameObject), [&]() { + Commands::DetachEntity(*m_context, gameObject); + }); + Actions::DrawMenuAction(Actions::MakeRenameEntityAction(gameObject), [&]() { + BeginRename(gameObject); + }); + Actions::DrawMenuAction(Actions::MakeDeleteEntityAction(gameObject), [&]() { + Commands::DeleteEntity(*m_context, gameObject->GetID()); + }); + Actions::DrawMenuSeparator(); + Actions::DrawMenuAction(Actions::MakeCopyEntityAction(gameObject), [&]() { + Commands::CopyEntity(*m_context, gameObject->GetID()); + }); + Actions::DrawMenuAction(Actions::MakePasteEntityAction(*m_context), [&]() { + Commands::PasteEntity(*m_context, gameObject->GetID()); + }); + Actions::DrawMenuAction(Actions::MakeDuplicateEntityAction(gameObject), [&]() { + Commands::DuplicateEntity(*m_context, gameObject->GetID()); + }); } void HierarchyPanel::RenderCreateMenu(::XCEngine::Components::GameObject* parent) { - auto& sceneManager = m_context->GetSceneManager(); - auto& selectionManager = m_context->GetSelectionManager(); - - if (ImGui::MenuItem("Empty Object")) { - UndoUtils::ExecuteSceneCommand(*m_context, "Create Entity", [&]() { - auto* newEntity = sceneManager.CreateEntity("GameObject", parent); - selectionManager.SetSelectedEntity(newEntity->GetID()); - }); + Actions::DrawMenuAction(Actions::MakeCreateEmptyEntityAction(), [&]() { + Commands::CreateEmptyEntity(*m_context, parent, "Create Entity", "GameObject"); + }); + Actions::DrawMenuSeparator(); + Actions::DrawMenuAction(Actions::MakeCreateCameraEntityAction(), [&]() { + Commands::CreateCameraEntity(*m_context, parent); + }); + Actions::DrawMenuAction(Actions::MakeCreateLightEntityAction(), [&]() { + Commands::CreateLightEntity(*m_context, parent); + }); + Actions::DrawMenuSeparator(); + Actions::DrawMenuAction(Actions::MakeCreateCubeEntityAction(), [&]() { + Commands::CreateEmptyEntity(*m_context, parent, "Create Cube", "Cube"); + }); + Actions::DrawMenuAction(Actions::MakeCreateSphereEntityAction(), [&]() { + Commands::CreateEmptyEntity(*m_context, parent, "Create Sphere", "Sphere"); + }); + Actions::DrawMenuAction(Actions::MakeCreatePlaneEntityAction(), [&]() { + Commands::CreateEmptyEntity(*m_context, parent, "Create Plane", "Plane"); + }); +} + +void HierarchyPanel::BeginRename(::XCEngine::Components::GameObject* gameObject) { + if (!gameObject) { + CancelRename(); + return; } - - ImGui::Separator(); - - if (ImGui::MenuItem("Camera")) { - UndoUtils::ExecuteSceneCommand(*m_context, "Create Camera", [&]() { - auto* newEntity = sceneManager.CreateEntity("Camera", parent); - newEntity->AddComponent<::XCEngine::Components::CameraComponent>(); - sceneManager.MarkSceneDirty(); - selectionManager.SetSelectedEntity(newEntity->GetID()); - }); - } - - if (ImGui::MenuItem("Light")) { - UndoUtils::ExecuteSceneCommand(*m_context, "Create Light", [&]() { - auto* newEntity = sceneManager.CreateEntity("Light", parent); - newEntity->AddComponent<::XCEngine::Components::LightComponent>(); - sceneManager.MarkSceneDirty(); - selectionManager.SetSelectedEntity(newEntity->GetID()); - }); - } - - ImGui::Separator(); - - if (ImGui::MenuItem("Cube")) { - UndoUtils::ExecuteSceneCommand(*m_context, "Create Cube", [&]() { - auto* newEntity = sceneManager.CreateEntity("Cube", parent); - selectionManager.SetSelectedEntity(newEntity->GetID()); - }); - } - - if (ImGui::MenuItem("Sphere")) { - UndoUtils::ExecuteSceneCommand(*m_context, "Create Sphere", [&]() { - auto* newEntity = sceneManager.CreateEntity("Sphere", parent); - selectionManager.SetSelectedEntity(newEntity->GetID()); - }); - } - - if (ImGui::MenuItem("Plane")) { - UndoUtils::ExecuteSceneCommand(*m_context, "Create Plane", [&]() { - auto* newEntity = sceneManager.CreateEntity("Plane", parent); - selectionManager.SetSelectedEntity(newEntity->GetID()); - }); + + m_renaming = true; + m_renamingEntity = gameObject; + strcpy_s(m_renameBuffer, gameObject->GetName().c_str()); + m_renameJustStarted = true; +} + +void HierarchyPanel::CommitRename() { + if (m_renamingEntity && strlen(m_renameBuffer) > 0) { + Commands::RenameEntity(*m_context, m_renamingEntity->GetID(), m_renameBuffer); } + + CancelRename(); +} + +void HierarchyPanel::CancelRename() { + m_renaming = false; + m_renamingEntity = nullptr; + m_renameBuffer[0] = '\0'; + m_renameJustStarted = false; } void HierarchyPanel::HandleDragDrop(::XCEngine::Components::GameObject* gameObject) { - auto& sceneManager = m_context->GetSceneManager(); - if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { ImGui::SetDragDropPayload("ENTITY_PTR", &gameObject, sizeof(::XCEngine::Components::GameObject*)); ImGui::Text("%s", gameObject->GetName().c_str()); @@ -365,32 +284,8 @@ void HierarchyPanel::HandleDragDrop(::XCEngine::Components::GameObject* gameObje if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_PTR")) { ::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data; - if (sourceGameObject != gameObject && sourceGameObject != nullptr) { - bool isValidMove = true; - ::XCEngine::Components::GameObject* checkParent = gameObject; - while (checkParent != nullptr) { - if (checkParent == sourceGameObject) { - isValidMove = false; - break; - } - checkParent = checkParent->GetParent(); - } - - if (isValidMove) { - UndoUtils::ExecuteSceneCommand(*m_context, "Reparent Entity", [&]() { - auto* srcTransform = sourceGameObject->GetTransform(); - Math::Vector3 worldPos = srcTransform->GetPosition(); - Math::Quaternion worldRot = srcTransform->GetRotation(); - Math::Vector3 worldScale = srcTransform->GetScale(); - - sceneManager.MoveEntity(sourceGameObject->GetID(), gameObject->GetID()); - - srcTransform->SetPosition(worldPos); - srcTransform->SetRotation(worldRot); - srcTransform->SetScale(worldScale); - sceneManager.MarkSceneDirty(); - }); - } + if (sourceGameObject != gameObject && Commands::CanReparentEntity(sourceGameObject, gameObject)) { + Commands::ReparentEntityPreserveWorldTransform(*m_context, sourceGameObject, gameObject->GetID()); } } ImGui::EndDragDropTarget(); @@ -404,53 +299,21 @@ void HierarchyPanel::HandleKeyboardShortcuts() { ::XCEngine::Components::GameObject* selectedGameObject = sceneManager.GetEntity(selectionManager.GetSelectedEntity()); if (ImGui::IsWindowFocused()) { - if (ImGui::IsKeyPressed(ImGuiKey_Delete)) { - if (selectedGameObject != nullptr) { - UndoUtils::ExecuteSceneCommand(*m_context, "Delete Entity", [&]() { - sceneManager.DeleteEntity(selectedGameObject->GetID()); - }); - } - } - - if (ImGui::IsKeyPressed(ImGuiKey_F2)) { - if (selectedGameObject != nullptr) { - m_renaming = true; - m_renamingEntity = selectedGameObject; - strcpy_s(m_renameBuffer, selectedGameObject->GetName().c_str()); - m_renameJustStarted = true; - } - } - - ImGuiIO& io = ImGui::GetIO(); - if (io.KeyCtrl) { - if (ImGui::IsKeyPressed(ImGuiKey_C)) { - if (selectedGameObject != nullptr) { - sceneManager.CopyEntity(selectedGameObject->GetID()); - } - } - - if (ImGui::IsKeyPressed(ImGuiKey_V)) { - if (sceneManager.HasClipboardData()) { - UndoUtils::ExecuteSceneCommand(*m_context, "Paste Entity", [&]() { - const uint64_t newId = sceneManager.PasteEntity(selectedGameObject ? selectedGameObject->GetID() : 0); - if (newId != 0) { - selectionManager.SetSelectedEntity(newId); - } - }); - } - } - - if (ImGui::IsKeyPressed(ImGuiKey_D)) { - if (selectedGameObject != nullptr) { - UndoUtils::ExecuteSceneCommand(*m_context, "Duplicate Entity", [&]() { - const uint64_t newId = sceneManager.DuplicateEntity(selectedGameObject->GetID()); - if (newId != 0) { - selectionManager.SetSelectedEntity(newId); - } - }); - } - } - } + Actions::HandleShortcut(Actions::MakeDeleteEntityAction(selectedGameObject), [&]() { + Commands::DeleteEntity(*m_context, selectedGameObject->GetID()); + }); + Actions::HandleShortcut(Actions::MakeRenameEntityAction(selectedGameObject), [&]() { + BeginRename(selectedGameObject); + }); + Actions::HandleShortcut(Actions::MakeCopyEntityAction(selectedGameObject), [&]() { + Commands::CopyEntity(*m_context, selectedGameObject->GetID()); + }); + Actions::HandleShortcut(Actions::MakePasteEntityAction(*m_context), [&]() { + Commands::PasteEntity(*m_context, selectedGameObject ? selectedGameObject->GetID() : 0); + }); + Actions::HandleShortcut(Actions::MakeDuplicateEntityAction(selectedGameObject), [&]() { + Commands::DuplicateEntity(*m_context, selectedGameObject->GetID()); + }); } } diff --git a/editor/src/panels/HierarchyPanel.h b/editor/src/panels/HierarchyPanel.h index 90460abb..fcdfbc02 100644 --- a/editor/src/panels/HierarchyPanel.h +++ b/editor/src/panels/HierarchyPanel.h @@ -23,6 +23,9 @@ private: void RenderEntity(::XCEngine::Components::GameObject* gameObject, const std::string& filter); void RenderContextMenu(::XCEngine::Components::GameObject* gameObject); void RenderCreateMenu(::XCEngine::Components::GameObject* parent); + void BeginRename(::XCEngine::Components::GameObject* gameObject); + void CommitRename(); + void CancelRename(); void HandleDragDrop(::XCEngine::Components::GameObject* gameObject); void HandleKeyboardShortcuts(); bool PassesFilter(::XCEngine::Components::GameObject* gameObject, const std::string& filter); diff --git a/editor/src/panels/InspectorPanel.cpp b/editor/src/panels/InspectorPanel.cpp index 8baefd6c..54c31fcc 100644 --- a/editor/src/panels/InspectorPanel.cpp +++ b/editor/src/panels/InspectorPanel.cpp @@ -1,14 +1,14 @@ +#include "Actions/EditorActions.h" +#include "Commands/ComponentCommands.h" #include "InspectorPanel.h" #include "Core/IEditorContext.h" #include "Core/ISceneManager.h" #include "Core/ISelectionManager.h" -#include "Core/IUndoManager.h" #include "Core/EventBus.h" #include "Core/EditorEvents.h" #include "ComponentEditors/ComponentEditorRegistry.h" #include "ComponentEditors/IComponentEditor.h" -#include "UI/PanelChrome.h" -#include "Utils/UndoUtils.h" +#include "UI/UI.h" #include #include @@ -52,14 +52,10 @@ void InspectorPanel::Render() { if (gameObject) { RenderGameObject(gameObject); } else { - ImGui::SetCursorPos(ImVec2(10.0f, 10.0f)); - ImGui::Text("Object not found"); + RenderEmptyState("Object not found"); } } else { - ImGui::SetCursorPos(ImVec2(10.0f, 10.0f)); - ImGui::Text("No Selection"); - ImGui::SetCursorPos(ImVec2(10.0f, 30.0f)); - ImGui::TextColored(ImVec4(0.55f, 0.55f, 0.55f, 1.0f), "Select an object in Hierarchy"); + RenderEmptyState("No Selection", "Select an object in Hierarchy"); } } @@ -81,7 +77,7 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb RenderComponent(component, gameObject); } - if (ImGui::Button("Add Component", ImVec2(-1.0f, 0.0f))) { + if (Actions::DrawInspectorAction(Actions::MakeAddComponentButtonAction(gameObject != nullptr))) { ImGui::OpenPopup("AddComponent"); } RenderAddComponentPopup(gameObject); @@ -91,13 +87,19 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb } } -void InspectorPanel::RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject) { - if (!ImGui::BeginPopup("AddComponent")) { +void InspectorPanel::RenderEmptyState(const char* title, const char* subtitle) { + UI::PanelContentScope content("InspectorEmptyState", UI::InspectorPanelContentPadding()); + if (!content.IsOpen()) { return; } - ImGui::Text("Components"); - ImGui::Separator(); + UI::DrawEmptyState(title, subtitle); +} + +void InspectorPanel::RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject) { + if (!UI::BeginTitledPopup("AddComponent", "Components")) { + return; + } bool drewAnyEntry = false; for (const auto& editor : ComponentEditorRegistry::Get().GetEditors()) { @@ -106,7 +108,7 @@ void InspectorPanel::RenderAddComponentPopup(::XCEngine::Components::GameObject* } drewAnyEntry = true; - const bool canAdd = editor->CanAddTo(gameObject); + const bool canAdd = Commands::CanAddComponent(*editor, gameObject); std::string label = editor->GetDisplayName(); if (!canAdd) { const char* reason = editor->GetAddDisabledReason(gameObject); @@ -117,88 +119,49 @@ void InspectorPanel::RenderAddComponentPopup(::XCEngine::Components::GameObject* } } - if (ImGui::MenuItem(label.c_str(), nullptr, false, canAdd)) { - bool added = false; - UndoUtils::ExecuteSceneCommand(*m_context, std::string("Add ") + editor->GetDisplayName() + " Component", [&]() { - added = editor->AddTo(gameObject) != nullptr; - if (added) { - m_context->GetSceneManager().MarkSceneDirty(); - } - }); - if (added) { + Actions::DrawMenuAction(Actions::MakeAddComponentMenuAction(label, canAdd), [&]() { + if (Commands::AddComponent(*m_context, *editor, gameObject)) { ImGui::CloseCurrentPopup(); } - } + }); } if (!drewAnyEntry) { - ImGui::TextDisabled("No registered component editors"); + UI::DrawHintText("No registered component editors"); } - ImGui::EndPopup(); + UI::EndTitledPopup(); } void InspectorPanel::RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject) { if (!component) return; IComponentEditor* editor = ComponentEditorRegistry::Get().FindEditor(component); - ImGuiStyle& style = ImGui::GetStyle(); - const std::string name = component->GetName(); - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{6, 4}); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x, 0.0f)); - - ImGuiTreeNodeFlags flags = - ImGuiTreeNodeFlags_DefaultOpen | - ImGuiTreeNodeFlags_Framed | - ImGuiTreeNodeFlags_SpanAvailWidth | - ImGuiTreeNodeFlags_FramePadding | - ImGuiTreeNodeFlags_AllowOverlap; - - bool open = ImGui::TreeNodeEx((void*)typeid(*component).hash_code(), flags, "%s", name.c_str()); - - ImGui::PopStyleVar(2); - - bool removeComponent = false; - const bool canRemoveComponent = editor ? editor->CanRemove(component) : false; - if (ImGui::BeginPopupContextItem("ComponentSettings")) { - if (ImGui::MenuItem("Remove Component", nullptr, false, canRemoveComponent)) { - removeComponent = true; - } - ImGui::EndPopup(); - } - - if (removeComponent) { - RemoveComponentByType(component, gameObject); + + const UI::ComponentSectionResult section = + UI::BeginComponentSection( + (void*)typeid(*component).hash_code(), + name.c_str(), + Commands::CanRemoveComponent(component, editor)); + + if (section.removeRequested) { + Commands::RemoveComponent(*m_context, component, gameObject, editor); return; } - if (open) { + if (section.open) { if (editor) { if (editor->Render(component, &m_context->GetUndoManager())) { m_context->GetSceneManager().MarkSceneDirty(); } } else { - ImGui::TextDisabled("No registered editor for this component"); + UI::DrawHintText("No registered editor for this component"); } - ImGui::TreePop(); + UI::EndComponentSection(); } } -void InspectorPanel::RemoveComponentByType(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject) { - if (!component || !gameObject) return; - - if (dynamic_cast<::XCEngine::Components::TransformComponent*>(component)) { - return; - } - - UndoUtils::ExecuteSceneCommand(*m_context, std::string("Remove ") + component->GetName() + " Component", [&]() { - gameObject->RemoveComponent(component); - m_context->GetSceneManager().MarkSceneDirty(); - }); -} - } } diff --git a/editor/src/panels/InspectorPanel.h b/editor/src/panels/InspectorPanel.h index f0268623..deb2e96a 100644 --- a/editor/src/panels/InspectorPanel.h +++ b/editor/src/panels/InspectorPanel.h @@ -23,7 +23,7 @@ private: void RenderGameObject(::XCEngine::Components::GameObject* gameObject); void RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject); void RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject); - void RemoveComponentByType(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject); + void RenderEmptyState(const char* title, const char* subtitle = nullptr); void OnSelectionChanged(const struct SelectionChangedEvent& event); uint64_t m_selectionHandlerId = 0; diff --git a/editor/src/panels/MenuBar.cpp b/editor/src/panels/MenuBar.cpp index 07f99636..05a51e69 100644 --- a/editor/src/panels/MenuBar.cpp +++ b/editor/src/panels/MenuBar.cpp @@ -1,9 +1,14 @@ +#include "Actions/EditorActions.h" +#include "Commands/EntityCommands.h" +#include "Commands/SceneCommands.h" #include "MenuBar.h" +#include "Core/EditorEvents.h" +#include "Core/EventBus.h" #include "Core/IEditorContext.h" #include "Core/ISceneManager.h" #include "Core/IUndoManager.h" #include "Core/ISelectionManager.h" -#include "Utils/SceneEditorUtils.h" +#include "UI/UI.h" #include #include @@ -23,56 +28,8 @@ void MenuBar::Render() { RenderSceneStatus(); ImGui::EndMainMenuBar(); } -} -void MenuBar::NewScene() { - if (!m_context || !SceneEditorUtils::ConfirmSceneSwitch(*m_context)) { - return; - } - - m_context->GetSceneManager().NewScene(); - m_context->GetSelectionManager().ClearSelection(); - m_context->GetUndoManager().ClearHistory(); -} - -void MenuBar::OpenScene() { - if (!m_context || !SceneEditorUtils::ConfirmSceneSwitch(*m_context)) { - return; - } - - const std::string filePath = SceneEditorUtils::OpenSceneFileDialog( - m_context->GetProjectPath(), - m_context->GetSceneManager().GetCurrentScenePath()); - if (!filePath.empty()) { - if (m_context->GetSceneManager().LoadScene(filePath)) { - m_context->GetSelectionManager().ClearSelection(); - m_context->GetUndoManager().ClearHistory(); - } - } -} - -void MenuBar::SaveScene() { - if (!m_context) { - return; - } - - if (!SceneEditorUtils::SaveCurrentScene(*m_context)) { - return; - } -} - -void MenuBar::SaveSceneAs() { - if (!m_context) { - return; - } - - const std::string filePath = SceneEditorUtils::SaveSceneFileDialog( - m_context->GetProjectPath(), - m_context->GetSceneManager().GetCurrentScenePath(), - m_context->GetSceneManager().GetCurrentSceneName()); - if (!filePath.empty() && m_context->GetSceneManager().SaveSceneAs(filePath)) { - m_context->GetProjectManager().RefreshCurrentFolder(); - } + RenderAboutPopup(); } void MenuBar::HandleShortcuts() { @@ -80,92 +37,83 @@ void MenuBar::HandleShortcuts() { return; } - ImGuiIO& io = ImGui::GetIO(); - if (!io.KeyCtrl) { - return; - } - - auto& undoManager = m_context->GetUndoManager(); - if (ImGui::IsKeyPressed(ImGuiKey_N, false)) { - NewScene(); - } - if (ImGui::IsKeyPressed(ImGuiKey_O, false)) { - OpenScene(); - } - if (ImGui::IsKeyPressed(ImGuiKey_S, false)) { - if (io.KeyShift) { - SaveSceneAs(); - } else { - SaveScene(); - } - } - if (!io.WantTextInput) { - if (ImGui::IsKeyPressed(ImGuiKey_Z, false)) { - if (io.KeyShift) { - if (undoManager.CanRedo()) { - undoManager.Redo(); - } - } else if (undoManager.CanUndo()) { - undoManager.Undo(); - } - } - if (ImGui::IsKeyPressed(ImGuiKey_Y, false) && undoManager.CanRedo()) { - undoManager.Redo(); - } - } + Actions::HandleShortcut(Actions::MakeNewSceneAction(), [&]() { Commands::NewScene(*m_context); }); + Actions::HandleShortcut(Actions::MakeOpenSceneAction(), [&]() { Commands::OpenSceneWithDialog(*m_context); }); + Actions::HandleShortcut(Actions::MakeSaveSceneAction(), [&]() { Commands::SaveCurrentScene(*m_context); }); + Actions::HandleShortcut(Actions::MakeSaveSceneAsAction(), [&]() { Commands::SaveSceneAsWithDialog(*m_context); }); + Actions::HandleShortcut(Actions::MakeUndoAction(*m_context), [&]() { m_context->GetUndoManager().Undo(); }); + Actions::HandleShortcut(Actions::MakeRedoAction(*m_context), [&]() { m_context->GetUndoManager().Redo(); }); } void MenuBar::ShowFileMenu() { - if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("New Scene", "Ctrl+N")) { - NewScene(); - } - if (ImGui::MenuItem("Open Scene", "Ctrl+O")) { - OpenScene(); - } - if (ImGui::MenuItem("Save Scene", "Ctrl+S")) { - SaveScene(); - } - if (ImGui::MenuItem("Save Scene As...", "Ctrl+Shift+S")) { - SaveSceneAs(); - } - ImGui::Separator(); - if (ImGui::MenuItem("Exit", "Alt+F4")) {} - ImGui::EndMenu(); - } + UI::DrawMenuScope("File", [&]() { + Actions::DrawMenuAction(Actions::MakeNewSceneAction(), [&]() { Commands::NewScene(*m_context); }); + Actions::DrawMenuAction(Actions::MakeOpenSceneAction(), [&]() { Commands::OpenSceneWithDialog(*m_context); }); + Actions::DrawMenuAction(Actions::MakeSaveSceneAction(), [&]() { Commands::SaveCurrentScene(*m_context); }); + Actions::DrawMenuAction(Actions::MakeSaveSceneAsAction(), [&]() { Commands::SaveSceneAsWithDialog(*m_context); }); + Actions::DrawMenuSeparator(); + Actions::DrawMenuAction(Actions::MakeExitAction(), [&]() { + m_context->GetEventBus().Publish(EditorExitRequestedEvent{}); + }); + }); } void MenuBar::ShowEditMenu() { - if (ImGui::BeginMenu("Edit")) { - auto& undoManager = m_context->GetUndoManager(); - const std::string undoLabel = undoManager.CanUndo() ? "Undo " + undoManager.GetUndoLabel() : "Undo"; - const std::string redoLabel = undoManager.CanRedo() ? "Redo " + undoManager.GetRedoLabel() : "Redo"; - if (ImGui::MenuItem(undoLabel.c_str(), "Ctrl+Z", false, undoManager.CanUndo())) { - undoManager.Undo(); - } - if (ImGui::MenuItem(redoLabel.c_str(), "Ctrl+Y", false, undoManager.CanRedo())) { - undoManager.Redo(); - } - ImGui::Separator(); - if (ImGui::MenuItem("Cut", "Ctrl+X")) {} - if (ImGui::MenuItem("Copy", "Ctrl+C")) {} - if (ImGui::MenuItem("Paste", "Ctrl+V")) {} - ImGui::EndMenu(); - } + ::XCEngine::Components::GameObject* selectedGameObject = Actions::GetSelectedGameObject(*m_context); + + UI::DrawMenuScope("Edit", [&]() { + Actions::DrawMenuAction(Actions::MakeUndoAction(*m_context), [&]() { m_context->GetUndoManager().Undo(); }); + Actions::DrawMenuAction(Actions::MakeRedoAction(*m_context), [&]() { m_context->GetUndoManager().Redo(); }); + Actions::DrawMenuSeparator(); + Actions::DrawMenuAction(Actions::MakeCutAction(false), []() {}); + Actions::DrawMenuAction(Actions::MakeCopyEntityAction(selectedGameObject), [&]() { + Commands::CopyEntity(*m_context, selectedGameObject->GetID()); + }); + Actions::DrawMenuAction(Actions::MakePasteEntityAction(*m_context), [&]() { + Commands::PasteEntity(*m_context, selectedGameObject ? selectedGameObject->GetID() : 0); + }); + }); } void MenuBar::ShowViewMenu() { - if (ImGui::BeginMenu("View")) { - if (ImGui::MenuItem("Reset Layout")) {} - ImGui::EndMenu(); - } + UI::DrawMenuScope("View", [&]() { + Actions::DrawMenuAction(Actions::MakeResetLayoutAction(), [&]() { + m_context->GetEventBus().Publish(DockLayoutResetRequestedEvent{}); + }); + }); } void MenuBar::ShowHelpMenu() { - if (ImGui::BeginMenu("Help")) { - if (ImGui::MenuItem("About")) {} - ImGui::EndMenu(); + UI::DrawMenuScope("Help", [&]() { + Actions::DrawMenuAction(Actions::MakeAboutAction(), [&]() { + m_aboutPopup.RequestOpen(); + }); + }); +} + +void MenuBar::RenderAboutPopup() { + m_aboutPopup.ConsumeOpenRequest("About XCEngine Editor"); + + if (!UI::BeginModalPopup("About XCEngine Editor")) { + return; } + + ImGui::TextUnformatted("XCEngine Editor"); + ImGui::Separator(); + UI::DrawHintText("Unity-like editor shell built on Dear ImGui."); + ImGui::Spacing(); + ImGui::Text("Date: 2026-03-26"); + ImGui::Text("UI Refactor: Actions / Commands / Layout in progress"); + if (m_context) { + ImGui::Text("Project: %s", m_context->GetProjectPath().c_str()); + } + ImGui::Spacing(); + + if (Actions::DrawButtonAction(Actions::MakeAction("Close"), UI::DialogActionButtonSize())) { + ImGui::CloseCurrentPopup(); + } + + UI::EndPopup(); } void MenuBar::RenderSceneStatus() { @@ -184,27 +132,18 @@ void MenuBar::RenderSceneStatus() { : std::filesystem::path(sceneManager.GetCurrentScenePath()).filename().string(); const bool dirty = sceneManager.IsSceneDirty(); - const std::string statusText = std::string("Scene: ") + fileLabel + (dirty ? " Modified" : " Saved"); - const ImVec2 textSize = ImGui::CalcTextSize(statusText.c_str()); - const float targetX = ImGui::GetWindowWidth() - textSize.x - 20.0f; - if (targetX > ImGui::GetCursorPosX()) { - ImGui::SetCursorPosX(targetX); - } - - const ImVec4 accentColor = dirty - ? ImVec4(0.94f, 0.68f, 0.20f, 1.0f) - : ImVec4(0.48f, 0.78f, 0.49f, 1.0f); - ImGui::TextColored(accentColor, "%s", statusText.c_str()); + const std::string statusText = dirty ? (std::string("* ") + fileLabel) : fileLabel; + UI::DrawRightAlignedText( + statusText.c_str(), + dirty ? UI::MenuBarStatusDirtyColor() : UI::MenuBarStatusIdleColor()); if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("Scene"); - ImGui::Separator(); + UI::BeginTitledTooltip("Scene"); ImGui::Text("Name: %s", sceneLabel.c_str()); ImGui::Text("File: %s", fileLabel.c_str()); ImGui::Text("State: %s", dirty ? "Modified" : "Saved"); ImGui::Text("Path: %s", sceneManager.GetCurrentScenePath().empty() ? "(not saved yet)" : sceneManager.GetCurrentScenePath().c_str()); - ImGui::EndTooltip(); + UI::EndTitledTooltip(); } } diff --git a/editor/src/panels/MenuBar.h b/editor/src/panels/MenuBar.h index bf0fea7b..99225f23 100644 --- a/editor/src/panels/MenuBar.h +++ b/editor/src/panels/MenuBar.h @@ -1,6 +1,7 @@ #pragma once #include "Panel.h" +#include "UI/PopupState.h" namespace XCEngine { namespace Editor { @@ -12,15 +13,14 @@ public: private: void HandleShortcuts(); - void NewScene(); - void OpenScene(); - void SaveScene(); - void SaveSceneAs(); void RenderSceneStatus(); + void RenderAboutPopup(); void ShowFileMenu(); void ShowEditMenu(); void ShowViewMenu(); void ShowHelpMenu(); + + UI::DeferredPopupState m_aboutPopup; }; } diff --git a/editor/src/panels/PanelCollection.h b/editor/src/panels/PanelCollection.h new file mode 100644 index 00000000..8173a1fc --- /dev/null +++ b/editor/src/panels/PanelCollection.h @@ -0,0 +1,85 @@ +#pragma once + +#include "Panel.h" + +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { + +class IEditorContext; + +class PanelCollection { +public: + void SetContext(IEditorContext* context) { + m_context = context; + + if (!m_context) { + return; + } + + for (const auto& panel : m_panels) { + if (!panel->HasContext()) { + panel->SetContext(m_context); + } + } + } + + template + TPanel& Emplace(Args&&... args) { + static_assert(std::is_base_of_v, "TPanel must derive from Panel"); + + auto panel = std::make_unique(std::forward(args)...); + if (m_context) { + panel->SetContext(m_context); + } + + TPanel* panelPtr = panel.get(); + m_panels.push_back(std::move(panel)); + return *panelPtr; + } + + void AttachAll() { + for (const auto& panel : m_panels) { + panel->OnAttach(); + } + } + + void DetachAll() { + for (auto it = m_panels.rbegin(); it != m_panels.rend(); ++it) { + (*it)->OnDetach(); + } + } + + void UpdateAll(float dt) { + for (const auto& panel : m_panels) { + panel->OnUpdate(dt); + } + } + + void DispatchEvent(void* event) { + for (const auto& panel : m_panels) { + panel->OnEvent(event); + } + } + + void RenderAll() { + for (const auto& panel : m_panels) { + panel->Render(); + } + } + + void Clear() { + m_panels.clear(); + } + +private: + IEditorContext* m_context = nullptr; + std::vector> m_panels; +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/panels/ProjectPanel.cpp b/editor/src/panels/ProjectPanel.cpp index aed8ed79..907e9304 100644 --- a/editor/src/panels/ProjectPanel.cpp +++ b/editor/src/panels/ProjectPanel.cpp @@ -1,13 +1,10 @@ +#include "Actions/EditorActions.h" +#include "Commands/ProjectCommands.h" #include "ProjectPanel.h" #include "Core/IEditorContext.h" #include "Core/IProjectManager.h" -#include "Core/ISceneManager.h" -#include "Core/IUndoManager.h" -#include "Core/ISelectionManager.h" #include "Core/AssetItem.h" -#include "UI/Core.h" -#include "UI/PanelChrome.h" -#include "Utils/SceneEditorUtils.h" +#include "UI/UI.h" #include #include @@ -16,31 +13,6 @@ namespace Editor { const char* DRAG_DROP_TYPE = "ASSET_ITEM"; -namespace { - -constexpr float kProjectToolbarHeight = 60.0f; - -void DrawFolderIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 fillColor, ImU32 lineColor) { - 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); -} - -void DrawFileIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 fillColor, ImU32 lineColor) { - 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, IM_COL32(235, 235, 235, 40)); - drawList->AddLine(foldA, foldB, lineColor); -} - -} // namespace - ProjectPanel::ProjectPanel() : Panel("Project") { } @@ -63,46 +35,24 @@ void ProjectPanel::Render() { auto& manager = m_context->GetProjectManager(); - UI::PanelToolbarScope toolbar("ProjectToolbar", kProjectToolbarHeight); + UI::PanelToolbarScope toolbar("ProjectToolbar", UI::ProjectPanelToolbarHeight()); if (toolbar.IsOpen()) { bool canGoBack = manager.CanNavigateBack(); - ImGui::BeginDisabled(!canGoBack); - if (UI::ToolbarButton("<", false, ImVec2(28.0f, 0.0f))) { - if (canGoBack) { - manager.NavigateBack(); - } + if (Actions::DrawToolbarAction(Actions::MakeNavigateBackAction(canGoBack), UI::ProjectBackButtonSize())) { + manager.NavigateBack(); } - ImGui::EndDisabled(); ImGui::SameLine(); - 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)); size_t pathDepth = manager.GetPathDepth(); - if (pathDepth == 0) { - ImGui::TextUnformatted("Assets"); - } else { - for (size_t i = 0; i < pathDepth; i++) { - if (i > 0) { - ImGui::SameLine(); - ImGui::TextDisabled("/"); - 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); + UI::DrawToolbarBreadcrumbs( + "Assets", + pathDepth, + [&](size_t index) { return manager.GetPathName(index); }, + [&](size_t index) { manager.NavigateToIndex(index); }); - ImGui::Dummy(ImVec2(0.0f, 2.0f)); - ImGui::SetNextItemWidth(-1.0f); - ImGui::InputTextWithHint("##Search", "Search assets", m_searchBuffer, sizeof(m_searchBuffer)); + UI::DrawToolbarRowGap(); + UI::ToolbarSearchField("##Search", "Search assets", m_searchBuffer, sizeof(m_searchBuffer)); } UI::PanelContentScope content( @@ -110,15 +60,15 @@ void ProjectPanel::Render() { UI::AssetPanelContentPadding(), ImGuiWindowFlags_None, true, - ImVec2(8.0f, 10.0f)); + UI::AssetGridSpacing()); if (!content.IsOpen()) { return; } - float buttonWidth = 104.0f; - float padding = 8.0f; - float panelWidth = ImGui::GetContentRegionAvail().x; - int columns = (int)(panelWidth / (buttonWidth + padding)); + const float tileWidth = UI::AssetTileSize().x; + const float spacing = UI::AssetGridSpacing().x; + const float panelWidth = ImGui::GetContentRegionAvail().x; + int columns = static_cast((panelWidth + spacing) / (tileWidth + spacing)); if (columns < 1) columns = 1; auto& items = manager.GetCurrentItems(); @@ -139,63 +89,60 @@ void ProjectPanel::Render() { displayedCount++; } + if (displayedCount == 0) { + UI::DrawEmptyState( + searchStr.empty() ? "No Assets" : "No Search Results", + searchStr.empty() ? "Current folder is empty" : "No assets match the current search"); + } + if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered()) { manager.SetSelectedIndex(-1); } - if (ImGui::BeginPopup("ItemContextMenu")) { + if (UI::BeginPopup("ItemContextMenu")) { if (m_contextMenuIndex >= 0 && m_contextMenuIndex < (int)items.size()) { auto& item = items[m_contextMenuIndex]; - if (item->isFolder || item->type == "Scene") { - if (ImGui::MenuItem("Open")) { - if (item->isFolder) { - manager.NavigateToFolder(item); - } else if (SceneEditorUtils::ConfirmSceneSwitch(*m_context)) { - if (m_context->GetSceneManager().LoadScene(item->fullPath)) { - m_context->GetSelectionManager().ClearSelection(); - m_context->GetUndoManager().ClearHistory(); - } - } - } - ImGui::Separator(); - } - if (ImGui::MenuItem("Delete")) { - manager.DeleteItem(m_contextMenuIndex); + const bool canOpen = item->isFolder || item->type == "Scene"; + Actions::DrawMenuAction(Actions::MakeOpenAssetAction(canOpen), [&]() { + Commands::OpenAsset(*m_context, item); + }); + Actions::DrawMenuSeparator(); + Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(), [&]() { + Commands::DeleteAsset(manager, m_contextMenuIndex); m_contextMenuIndex = -1; - } + }); } - ImGui::EndPopup(); + UI::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::EndPopup(); + if (UI::BeginPopup("EmptyContextMenu")) { + Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() { + m_createFolderDialog.RequestOpen("NewFolder"); + }); + UI::EndPopup(); } - if (m_showCreateFolderPopup) { - ImGui::OpenPopup("Create Folder"); - m_showCreateFolderPopup = false; - } + m_createFolderDialog.ConsumeOpenRequest("Create Folder"); - if (ImGui::BeginPopupModal("Create Folder", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::InputText("Name", m_newFolderName, sizeof(m_newFolderName)); + if (UI::BeginModalPopup("Create Folder")) { + ImGui::InputText("Name", m_createFolderDialog.Buffer(), m_createFolderDialog.BufferSize()); ImGui::Separator(); - if (ImGui::Button("Create", ImVec2(80, 0))) { - CreateNewFolder(m_newFolderName); - ImGui::CloseCurrentPopup(); + const Actions::ActionBinding createAction = Actions::MakeConfirmCreateAction(!m_createFolderDialog.Empty()); + const Actions::ActionBinding cancelAction = Actions::MakeCancelAction(); + if (Actions::DrawButtonAction(createAction, UI::DialogActionButtonSize())) { + if (Commands::CreateFolder(manager, m_createFolderDialog.Buffer())) { + ImGui::CloseCurrentPopup(); + } } ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(80, 0))) { + if (Actions::DrawButtonAction(cancelAction, UI::DialogActionButtonSize())) { ImGui::CloseCurrentPopup(); } - ImGui::EndPopup(); + UI::EndPopup(); } } @@ -204,67 +151,33 @@ void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) { bool isSelected = (manager.GetSelectedIndex() == index); ImGui::PushID(index); + const bool isDraggingThisItem = !m_draggingPath.empty() && item->fullPath == m_draggingPath; + const UI::AssetIconKind iconKind = item->isFolder ? UI::AssetIconKind::Folder : UI::AssetIconKind::File; - ImVec2 buttonSize(104.0f, 82.0f); + const UI::AssetTileResult tile = UI::DrawAssetTile( + item->name.c_str(), + isSelected, + isDraggingThisItem, + [&](ImDrawList* drawList, const ImVec2& iconMin, const ImVec2& iconMax) { + UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind); + }); - ImGui::InvisibleButton("##AssetBtn", buttonSize); - if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + if (tile.clicked) { manager.SetSelectedIndex(index); } - - bool doubleClicked = false; - if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { - doubleClicked = true; - } - + bool openContextMenu = false; - if (ImGui::IsItemClicked(1)) { + if (tile.contextRequested) { manager.SetSelectedIndex(index); m_contextMenuIndex = index; openContextMenu = true; } - ImVec2 min = ImGui::GetItemRectMin(); - ImVec2 max = ImVec2(min.x + buttonSize.x, min.y + buttonSize.y); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); - if (hovered || isSelected) { - drawList->AddRectFilled(min, max, isSelected ? IM_COL32(156, 156, 156, 30) : IM_COL32(255, 255, 255, 10), 2.0f); - } - if (isSelected) { - drawList->AddRect(min, max, IM_COL32(188, 188, 188, 110), 2.0f); - } - - if (!m_draggingPath.empty() && item->fullPath == m_draggingPath) { - drawList->AddRectFilled(min, max, IM_COL32(0, 0, 0, 60), 0.0f); - } - - ImU32 iconFillColor = item->isFolder ? IM_COL32(118, 118, 118, 255) : IM_COL32(104, 104, 104, 255); - ImU32 iconLineColor = item->isFolder ? IM_COL32(184, 184, 184, 220) : IM_COL32(166, 166, 166, 220); - - const ImVec2 iconMin(min.x + 14.0f, min.y + 12.0f); - const ImVec2 iconMax(iconMin.x + 28.0f, iconMin.y + 22.0f); - if (item->isFolder) { - DrawFolderIcon(drawList, iconMin, iconMax, iconFillColor, iconLineColor); - } else { - DrawFileIcon(drawList, iconMin, iconMax, iconFillColor, iconLineColor); - } - - ImVec4 textColor = isSelected ? ImVec4(0.93f, 0.93f, 0.93f, 1.0f) : ImVec4(0.76f, 0.76f, 0.76f, 1.0f); - ImVec2 textSize = ImGui::CalcTextSize(item->name.c_str()); - float textY = max.y - textSize.y - 10.0f; - - ImGui::PushClipRect(ImVec2(min.x + 6.0f, min.y), ImVec2(max.x - 6.0f, max.y), true); - drawList->AddText(ImVec2(min.x + 6.0f, textY), 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); + Commands::MoveAssetToFolder(manager, draggedPath, item); } ImGui::EndDragDropTarget(); } @@ -275,28 +188,16 @@ void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) { ImGui::SetDragDropPayload(DRAG_DROP_TYPE, item->fullPath.c_str(), item->fullPath.length() + 1); ImVec2 previewMin = ImGui::GetMousePos(); - ImVec2 previewMax = ImVec2(previewMin.x + 24.0f, previewMin.y + 20.0f); - if (item->isFolder) { - DrawFolderIcon(ImGui::GetForegroundDrawList(), previewMin, previewMax, iconFillColor, iconLineColor); - } else { - DrawFileIcon(ImGui::GetForegroundDrawList(), previewMin, previewMax, iconFillColor, iconLineColor); - } + const ImVec2 previewSize = UI::AssetDragPreviewSize(); + ImVec2 previewMax = ImVec2(previewMin.x + previewSize.x, previewMin.y + previewSize.y); + UI::DrawAssetIcon(ImGui::GetForegroundDrawList(), previewMin, previewMax, iconKind); ImGui::EndDragDropSource(); } } - if (doubleClicked) { - if (item->isFolder) { - manager.NavigateToFolder(item); - } else if (item->type == "Scene") { - if (SceneEditorUtils::ConfirmSceneSwitch(*m_context)) { - if (m_context->GetSceneManager().LoadScene(item->fullPath)) { - m_context->GetSelectionManager().ClearSelection(); - m_context->GetUndoManager().ClearHistory(); - } - } - } + if (tile.openRequested) { + Commands::OpenAsset(*m_context, item); } ImGui::PopID(); @@ -306,14 +207,5 @@ void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) { } } -void ProjectPanel::CreateNewFolder(const std::string& name) { - auto& manager = m_context->GetProjectManager(); - manager.CreateFolder(name); -} - -bool ProjectPanel::HandleDrop(const AssetItemPtr& targetFolder) { - return false; -} - } } diff --git a/editor/src/panels/ProjectPanel.h b/editor/src/panels/ProjectPanel.h index a5123b20..3e55b81b 100644 --- a/editor/src/panels/ProjectPanel.h +++ b/editor/src/panels/ProjectPanel.h @@ -2,6 +2,7 @@ #include "Panel.h" #include "Core/AssetItem.h" +#include "UI/PopupState.h" namespace XCEngine { namespace Editor { @@ -14,15 +15,12 @@ public: 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"; + UI::TextInputPopupState<256> m_createFolderDialog; int m_contextMenuIndex = -1; std::string m_draggingPath; }; } -} \ No newline at end of file +} diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index 5b1036b5..a4f5e79a 100644 --- a/editor/src/panels/SceneViewPanel.cpp +++ b/editor/src/panels/SceneViewPanel.cpp @@ -1,5 +1,5 @@ #include "SceneViewPanel.h" -#include "UI/PanelChrome.h" +#include "UI/UI.h" #include namespace XCEngine { diff --git a/editor/src/panels/SceneViewPanel.h b/editor/src/panels/SceneViewPanel.h index ab09cf06..3e8976a9 100644 --- a/editor/src/panels/SceneViewPanel.h +++ b/editor/src/panels/SceneViewPanel.h @@ -9,10 +9,7 @@ class SceneViewPanel : public Panel { public: SceneViewPanel(); void Render() override; - -private: - void RenderGrid(); }; } -} \ No newline at end of file +} diff --git a/editor/tab.png b/editor/tab.png new file mode 100644 index 00000000..1becf22d Binary files /dev/null and b/editor/tab.png differ diff --git a/editor/unity.png b/editor/unity.png new file mode 100644 index 00000000..32d51c66 Binary files /dev/null and b/editor/unity.png differ