From 75ded6f630d497ba4aec8446495d30579d43b148 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 4 Apr 2026 19:22:10 +0800 Subject: [PATCH] Add XCUI ImGui transition backend MVP --- ...ubplan-05_XCUI-ImGui-Transition-Backend.md | 47 ++++ editor/CMakeLists.txt | 1 + editor/src/Core/EditorWorkspace.h | 2 + .../src/XCUIBackend/ImGuiTransitionBackend.h | 188 ++++++++++++++++ editor/src/panels/XCUIDemoPanel.cpp | 140 ++++++++++++ editor/src/panels/XCUIDemoPanel.h | 20 ++ engine/include/XCEngine/UI/DrawData.h | 204 ++++++++++++++++++ tests/editor/CMakeLists.txt | 2 + tests/editor/test_xcui_draw_data.cpp | 53 +++++ .../test_xcui_imgui_transition_backend.cpp | 97 +++++++++ 10 files changed, 754 insertions(+) create mode 100644 docs/plan/xcui-subplans/Subplan-05_XCUI-ImGui-Transition-Backend.md create mode 100644 editor/src/XCUIBackend/ImGuiTransitionBackend.h create mode 100644 editor/src/panels/XCUIDemoPanel.cpp create mode 100644 editor/src/panels/XCUIDemoPanel.h create mode 100644 engine/include/XCEngine/UI/DrawData.h create mode 100644 tests/editor/test_xcui_draw_data.cpp create mode 100644 tests/editor/test_xcui_imgui_transition_backend.cpp diff --git a/docs/plan/xcui-subplans/Subplan-05_XCUI-ImGui-Transition-Backend.md b/docs/plan/xcui-subplans/Subplan-05_XCUI-ImGui-Transition-Backend.md new file mode 100644 index 00000000..4264455b --- /dev/null +++ b/docs/plan/xcui-subplans/Subplan-05_XCUI-ImGui-Transition-Backend.md @@ -0,0 +1,47 @@ +# Subplan 05:XCUI ImGui Transition Backend + +目标: + +- 在过渡阶段,用 ImGui 只做宿主窗口与 draw submission 容器。 +- 让 XCUI 自己生成 draw list,再由 ImGui backend 负责落屏。 + +负责人边界: + +- 负责 `editor/src/XCUIBackend/` 或等价新目录。 +- 负责 XCUI draw primitive 到 ImGui draw call 的映射。 +- 不负责 XCUI tree、布局、样式的内部规则。 + +建议目录: + +- `editor/src/XCUIBackend/` +- `editor/src/UI/` 中与 XCUI backend 直接相关的桥接代码 +- `tests/Editor` 中 backend 相关测试 + +前置依赖: + +- 需要 `Subplan 01` 给出稳定 draw data / frame submission 契约。 +- 需要 `Subplan 03` 提供样式查询结果。 + +现在就可以先做的内容: + +- 定义 `UIDrawList` / `UIDrawCommand` / `UIDrawText` / `UIDrawImage` / `UIDrawClip` +- 先做矩形、边框、文字、图片四类 primitive +- 设计 frame begin / submit / end 的 adapter 流程 +- 写一个最小 demo panel,用 XCUI draw list 通过 ImGui 显示 + +明确不做: + +- 不做 RHI native backend +- 不做 docking 逻辑 + +交付物: + +- XCUI -> ImGui 过渡 backend +- primitive 转换测试或快照测试 +- 最小 editor 接入样例 + +验收标准: + +- XCUI 逻辑层不直接依赖 ImGui API +- ImGui 只出现在 backend 适配层 +- 可以渲染基础控件和文本 diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 979cf93d..ef4e9602 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -100,6 +100,7 @@ add_executable(${PROJECT_NAME} WIN32 src/panels/InspectorPanel.cpp src/panels/ConsolePanel.cpp src/panels/ProjectPanel.cpp + src/panels/XCUIDemoPanel.cpp src/UI/BuiltInIcons.cpp src/Layers/EditorLayer.cpp ${IMGUI_SOURCES} diff --git a/editor/src/Core/EditorWorkspace.h b/editor/src/Core/EditorWorkspace.h index abc44d82..5524149d 100644 --- a/editor/src/Core/EditorWorkspace.h +++ b/editor/src/Core/EditorWorkspace.h @@ -12,6 +12,7 @@ #include "panels/PanelCollection.h" #include "panels/ProjectPanel.h" #include "panels/SceneViewPanel.h" +#include "panels/XCUIDemoPanel.h" #include @@ -34,6 +35,7 @@ public: m_panels.Emplace(); m_panels.Emplace(); m_projectPanel = &m_panels.Emplace(); + m_panels.Emplace(); m_dockLayoutController = std::make_unique(); m_projectPanel->Initialize(context.GetProjectPath()); diff --git a/editor/src/XCUIBackend/ImGuiTransitionBackend.h b/editor/src/XCUIBackend/ImGuiTransitionBackend.h new file mode 100644 index 00000000..1a1f1ba5 --- /dev/null +++ b/editor/src/XCUIBackend/ImGuiTransitionBackend.h @@ -0,0 +1,188 @@ +#pragma once + +#include + +#include + +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +class ImGuiTransitionBackend { +public: + void BeginFrame() { + m_lastFlushedDrawListCount = 0; + m_lastFlushedCommandCount = 0; + m_pendingCommandCount = 0; + m_pendingDrawLists.clear(); + } + + void Submit(const ::XCEngine::UI::UIDrawList& drawList) { + m_pendingCommandCount += drawList.GetCommandCount(); + m_pendingDrawLists.push_back(drawList); + } + + void Submit(::XCEngine::UI::UIDrawList&& drawList) { + m_pendingCommandCount += drawList.GetCommandCount(); + m_pendingDrawLists.push_back(std::move(drawList)); + } + + void Submit(const ::XCEngine::UI::UIDrawData& drawData) { + for (const ::XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { + Submit(drawList); + } + } + + bool HasPendingDrawData() const { + return !m_pendingDrawLists.empty(); + } + + std::size_t GetPendingDrawListCount() const { + return m_pendingDrawLists.size(); + } + + std::size_t GetPendingCommandCount() const { + return m_pendingCommandCount; + } + + std::size_t GetLastFlushedDrawListCount() const { + return m_lastFlushedDrawListCount; + } + + std::size_t GetLastFlushedCommandCount() const { + return m_lastFlushedCommandCount; + } + + bool EndFrame(ImDrawList* targetDrawList = nullptr) { + ImDrawList* drawList = targetDrawList != nullptr ? targetDrawList : ImGui::GetWindowDrawList(); + if (drawList == nullptr) { + ClearPendingState(); + return false; + } + + std::size_t clipDepth = 0; + for (const ::XCEngine::UI::UIDrawList& pendingDrawList : m_pendingDrawLists) { + for (const ::XCEngine::UI::UIDrawCommand& command : pendingDrawList.GetCommands()) { + RenderCommand(*drawList, command, clipDepth); + } + } + + while (clipDepth > 0) { + drawList->PopClipRect(); + --clipDepth; + } + + m_lastFlushedDrawListCount = m_pendingDrawLists.size(); + m_lastFlushedCommandCount = m_pendingCommandCount; + ClearPendingState(); + return true; + } + +private: + static ImVec2 ToImVec2(const ::XCEngine::UI::UIPoint& point) { + return ImVec2(point.x, point.y); + } + + static ImVec2 ToImVec2Min(const ::XCEngine::UI::UIRect& rect) { + return ImVec2(rect.x, rect.y); + } + + static ImVec2 ToImVec2Max(const ::XCEngine::UI::UIRect& rect) { + return ImVec2(rect.x + rect.width, rect.y + rect.height); + } + + static ImTextureID ToImTextureId(const ::XCEngine::UI::UITextureHandle& texture) { + return static_cast(texture.nativeHandle); + } + + static ImU32 ToImU32(const ::XCEngine::UI::UIColor& color) { + const float r = (std::max)(0.0f, (std::min)(1.0f, color.r)); + const float g = (std::max)(0.0f, (std::min)(1.0f, color.g)); + const float b = (std::max)(0.0f, (std::min)(1.0f, color.b)); + const float a = (std::max)(0.0f, (std::min)(1.0f, color.a)); + return IM_COL32( + static_cast(r * 255.0f), + static_cast(g * 255.0f), + static_cast(b * 255.0f), + static_cast(a * 255.0f)); + } + + static void RenderCommand( + ImDrawList& drawList, + const ::XCEngine::UI::UIDrawCommand& command, + std::size_t& clipDepth) { + switch (command.type) { + case ::XCEngine::UI::UIDrawCommandType::FilledRect: + drawList.AddRectFilled( + ToImVec2Min(command.rect), + ToImVec2Max(command.rect), + ToImU32(command.color), + command.rounding); + break; + case ::XCEngine::UI::UIDrawCommandType::RectOutline: + drawList.AddRect( + ToImVec2Min(command.rect), + ToImVec2Max(command.rect), + ToImU32(command.color), + command.rounding, + 0, + command.thickness > 0.0f ? command.thickness : 1.0f); + break; + case ::XCEngine::UI::UIDrawCommandType::Text: + if (!command.text.empty()) { + drawList.AddText( + nullptr, + command.fontSize > 0.0f ? command.fontSize : ImGui::GetFontSize(), + ToImVec2(command.position), + ToImU32(command.color), + command.text.c_str()); + } + break; + case ::XCEngine::UI::UIDrawCommandType::Image: + if (command.texture.IsValid()) { + drawList.AddImage( + ToImTextureId(command.texture), + ToImVec2Min(command.rect), + ToImVec2Max(command.rect), + ImVec2(0.0f, 0.0f), + ImVec2(1.0f, 1.0f), + ToImU32(command.color)); + } + break; + case ::XCEngine::UI::UIDrawCommandType::PushClipRect: + drawList.PushClipRect( + ToImVec2Min(command.rect), + ToImVec2Max(command.rect), + command.intersectWithCurrentClip); + ++clipDepth; + break; + case ::XCEngine::UI::UIDrawCommandType::PopClipRect: + if (clipDepth > 0) { + drawList.PopClipRect(); + --clipDepth; + } + break; + default: + break; + } + } + + void ClearPendingState() { + m_pendingCommandCount = 0; + m_pendingDrawLists.clear(); + } + + std::vector<::XCEngine::UI::UIDrawList> m_pendingDrawLists; + std::size_t m_pendingCommandCount = 0; + std::size_t m_lastFlushedDrawListCount = 0; + std::size_t m_lastFlushedCommandCount = 0; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/panels/XCUIDemoPanel.cpp b/editor/src/panels/XCUIDemoPanel.cpp new file mode 100644 index 00000000..9b6e41de --- /dev/null +++ b/editor/src/panels/XCUIDemoPanel.cpp @@ -0,0 +1,140 @@ +#include "XCUIDemoPanel.h" + +#include "UI/UI.h" + +#include + +#include + +namespace { + +::XCEngine::UI::UIDrawList BuildXCUIDemoDrawList( + const ImVec2& canvasMin, + const ImVec2& canvasSize) { + using ::XCEngine::UI::UIColor; + using ::XCEngine::UI::UIDrawList; + using ::XCEngine::UI::UIPoint; + using ::XCEngine::UI::UIRect; + + UIDrawList drawList("XCUI Transition Demo"); + drawList.PushClipRect(UIRect(canvasMin.x, canvasMin.y, canvasSize.x, canvasSize.y)); + drawList.AddFilledRect( + UIRect(canvasMin.x, canvasMin.y, canvasSize.x, canvasSize.y), + UIColor(0.09f, 0.11f, 0.14f, 1.0f), + 10.0f); + drawList.AddRectOutline( + UIRect(canvasMin.x, canvasMin.y, canvasSize.x, canvasSize.y), + UIColor(0.32f, 0.40f, 0.49f, 1.0f), + 1.0f, + 10.0f); + + const float cardX = canvasMin.x + 18.0f; + const float cardY = canvasMin.y + 18.0f; + const float cardWidth = (canvasSize.x - 36.0f) > 120.0f ? (canvasSize.x - 36.0f) : 120.0f; + drawList.AddFilledRect( + UIRect(cardX, cardY, cardWidth, 56.0f), + UIColor(0.15f, 0.20f, 0.27f, 1.0f), + 8.0f); + drawList.AddFilledRect( + UIRect(cardX + 14.0f, cardY + 16.0f, 10.0f, 10.0f), + UIColor(0.18f, 0.76f, 0.58f, 1.0f), + 5.0f); + drawList.AddText( + UIPoint(cardX + 32.0f, cardY + 12.0f), + "XCUI Transition Backend", + UIColor(0.96f, 0.97f, 0.99f, 1.0f), + 18.0f); + drawList.AddText( + UIPoint(cardX + 32.0f, cardY + 33.0f), + "This panel is generated by XCUI draw commands, then flushed through ImGui.", + UIColor(0.74f, 0.79f, 0.86f, 1.0f), + 13.0f); + + const float statY = cardY + 76.0f; + drawList.AddFilledRect( + UIRect(cardX, statY, 140.0f, 74.0f), + UIColor(0.11f, 0.16f, 0.22f, 1.0f), + 8.0f); + drawList.AddFilledRect( + UIRect(cardX + 158.0f, statY, 180.0f, 74.0f), + UIColor(0.11f, 0.16f, 0.22f, 1.0f), + 8.0f); + drawList.AddText( + UIPoint(cardX + 14.0f, statY + 12.0f), + "Primitive Set", + UIColor(0.68f, 0.75f, 0.84f, 1.0f), + 13.0f); + drawList.AddText( + UIPoint(cardX + 14.0f, statY + 36.0f), + "Rect / Border / Text / Image", + UIColor(0.95f, 0.97f, 0.99f, 1.0f), + 15.0f); + drawList.AddText( + UIPoint(cardX + 172.0f, statY + 12.0f), + "Backend Flow", + UIColor(0.68f, 0.75f, 0.84f, 1.0f), + 13.0f); + drawList.AddText( + UIPoint(cardX + 172.0f, statY + 36.0f), + "UIDrawList -> ImGuiTransitionBackend -> ImDrawList", + UIColor(0.95f, 0.97f, 0.99f, 1.0f), + 15.0f); + + const float footerY = canvasMin.y + canvasSize.y - 54.0f; + if (footerY > statY + 90.0f) { + drawList.AddFilledRect( + UIRect(cardX, footerY, cardWidth, 36.0f), + UIColor(0.14f, 0.17f, 0.22f, 1.0f), + 8.0f); + drawList.AddText( + UIPoint(cardX + 14.0f, footerY + 10.0f), + "Subplan-05 MVP: XCUI owns draw data, ImGui is only the transition renderer.", + UIColor(0.79f, 0.84f, 0.90f, 1.0f), + 13.0f); + } + + drawList.PopClipRect(); + return drawList; +} + +} // namespace + +namespace XCEngine { +namespace Editor { + +XCUIDemoPanel::XCUIDemoPanel() + : Panel("XCUI Demo") { +} + +void XCUIDemoPanel::Render() { + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + UI::PanelWindowScope panel(m_name.c_str()); + ImGui::PopStyleVar(); + if (!panel.IsOpen()) { + return; + } + + UI::PanelContentScope content("XCUIDemoContent", ImVec2(0.0f, 0.0f)); + if (!content.IsOpen()) { + return; + } + + const ImVec2 availableSize = ImGui::GetContentRegionAvail(); + if (availableSize.x <= 1.0f || availableSize.y <= 1.0f) { + ImGui::Dummy(ImVec2(0.0f, 0.0f)); + return; + } + + const ImVec2 canvasMin = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("##XCUIDemoCanvas", availableSize); + + ::XCEngine::UI::UIDrawData drawData = {}; + drawData.AddDrawList(BuildXCUIDemoDrawList(canvasMin, availableSize)); + + m_backend.BeginFrame(); + m_backend.Submit(drawData); + m_backend.EndFrame(ImGui::GetWindowDrawList()); +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/panels/XCUIDemoPanel.h b/editor/src/panels/XCUIDemoPanel.h new file mode 100644 index 00000000..49cea95b --- /dev/null +++ b/editor/src/panels/XCUIDemoPanel.h @@ -0,0 +1,20 @@ +#pragma once + +#include "Panel.h" + +#include "XCUIBackend/ImGuiTransitionBackend.h" + +namespace XCEngine { +namespace Editor { + +class XCUIDemoPanel : public Panel { +public: + XCUIDemoPanel(); + void Render() override; + +private: + XCUIBackend::ImGuiTransitionBackend m_backend; +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/engine/include/XCEngine/UI/DrawData.h b/engine/include/XCEngine/UI/DrawData.h new file mode 100644 index 00000000..40dbf7b9 --- /dev/null +++ b/engine/include/XCEngine/UI/DrawData.h @@ -0,0 +1,204 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace UI { + +struct UIColor { + float r = 1.0f; + float g = 1.0f; + float b = 1.0f; + float a = 1.0f; + + constexpr UIColor() = default; + constexpr UIColor(float red, float green, float blue, float alpha = 1.0f) + : r(red) + , g(green) + , b(blue) + , a(alpha) { + } +}; + +enum class UIDrawCommandType : std::uint8_t { + FilledRect = 0, + RectOutline, + Text, + Image, + PushClipRect, + PopClipRect +}; + +struct UIDrawCommand { + UIDrawCommandType type = UIDrawCommandType::FilledRect; + UIRect rect = {}; + UIPoint position = {}; + UIColor color = {}; + float thickness = 1.0f; + float rounding = 0.0f; + float fontSize = 0.0f; + bool intersectWithCurrentClip = true; + UITextureHandle texture = {}; + std::string text; +}; + +class UIDrawList { +public: + UIDrawList() = default; + explicit UIDrawList(std::string debugName) + : m_debugName(std::move(debugName)) { + } + + const std::string& GetDebugName() const { + return m_debugName; + } + + void SetDebugName(std::string debugName) { + m_debugName = std::move(debugName); + } + + void Clear() { + m_commands.clear(); + } + + bool Empty() const { + return m_commands.empty(); + } + + std::size_t GetCommandCount() const { + return m_commands.size(); + } + + const std::vector& GetCommands() const { + return m_commands; + } + + UIDrawCommand& AddFilledRect( + const UIRect& rect, + const UIColor& color, + float rounding = 0.0f) { + UIDrawCommand command = {}; + command.type = UIDrawCommandType::FilledRect; + command.rect = rect; + command.color = color; + command.rounding = rounding; + return AddCommand(std::move(command)); + } + + UIDrawCommand& AddRectOutline( + const UIRect& rect, + const UIColor& color, + float thickness = 1.0f, + float rounding = 0.0f) { + UIDrawCommand command = {}; + command.type = UIDrawCommandType::RectOutline; + command.rect = rect; + command.color = color; + command.thickness = thickness; + command.rounding = rounding; + return AddCommand(std::move(command)); + } + + UIDrawCommand& AddText( + const UIPoint& position, + std::string text, + const UIColor& color = {}, + float fontSize = 0.0f) { + UIDrawCommand command = {}; + command.type = UIDrawCommandType::Text; + command.position = position; + command.color = color; + command.fontSize = fontSize; + command.text = std::move(text); + return AddCommand(std::move(command)); + } + + UIDrawCommand& AddImage( + const UIRect& rect, + const UITextureHandle& texture, + const UIColor& tintColor = {}) { + UIDrawCommand command = {}; + command.type = UIDrawCommandType::Image; + command.rect = rect; + command.texture = texture; + command.color = tintColor; + return AddCommand(std::move(command)); + } + + UIDrawCommand& PushClipRect( + const UIRect& rect, + bool intersectWithCurrentClip = true) { + UIDrawCommand command = {}; + command.type = UIDrawCommandType::PushClipRect; + command.rect = rect; + command.intersectWithCurrentClip = intersectWithCurrentClip; + return AddCommand(std::move(command)); + } + + UIDrawCommand& PopClipRect() { + UIDrawCommand command = {}; + command.type = UIDrawCommandType::PopClipRect; + return AddCommand(std::move(command)); + } + +private: + UIDrawCommand& AddCommand(UIDrawCommand&& command) { + m_commands.push_back(std::move(command)); + return m_commands.back(); + } + + std::string m_debugName; + std::vector m_commands; +}; + +class UIDrawData { +public: + void Clear() { + m_drawLists.clear(); + } + + bool Empty() const { + return m_drawLists.empty(); + } + + std::size_t GetDrawListCount() const { + return m_drawLists.size(); + } + + std::size_t GetTotalCommandCount() const { + std::size_t count = 0; + for (const UIDrawList& drawList : m_drawLists) { + count += drawList.GetCommandCount(); + } + return count; + } + + const std::vector& GetDrawLists() const { + return m_drawLists; + } + + UIDrawList& EmplaceDrawList(std::string debugName = {}) { + m_drawLists.emplace_back(std::move(debugName)); + return m_drawLists.back(); + } + + void AddDrawList(const UIDrawList& drawList) { + m_drawLists.push_back(drawList); + } + + void AddDrawList(UIDrawList&& drawList) { + m_drawLists.push_back(std::move(drawList)); + } + +private: + std::vector m_drawLists; +}; + +} // namespace UI +} // namespace XCEngine diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index b0ae4707..8d2e8e61 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -29,6 +29,8 @@ set(EDITOR_TEST_SOURCES test_viewport_render_targets.cpp test_viewport_render_flow_utils.cpp test_builtin_icon_layout_utils.cpp + test_xcui_draw_data.cpp + test_xcui_imgui_transition_backend.cpp test_editor_console_sink.cpp test_editor_script_assembly_builder.cpp test_editor_script_assembly_builder_utils.cpp diff --git a/tests/editor/test_xcui_draw_data.cpp b/tests/editor/test_xcui_draw_data.cpp new file mode 100644 index 00000000..770891f1 --- /dev/null +++ b/tests/editor/test_xcui_draw_data.cpp @@ -0,0 +1,53 @@ +#include + +#include + +TEST(XCUIDrawDataTest, DrawListBuilderPreservesCommandOrderAndPayload) { + using XCEngine::UI::UIColor; + using XCEngine::UI::UIDrawCommandType; + using XCEngine::UI::UIDrawList; + using XCEngine::UI::UIPoint; + using XCEngine::UI::UIRect; + using XCEngine::UI::UITextureHandle; + + UIDrawList drawList("PrimaryList"); + drawList.PushClipRect(UIRect(0.0f, 0.0f, 200.0f, 120.0f)); + drawList.AddFilledRect(UIRect(10.0f, 12.0f, 48.0f, 24.0f), UIColor(0.1f, 0.2f, 0.3f, 1.0f), 4.0f); + drawList.AddRectOutline(UIRect(10.0f, 12.0f, 48.0f, 24.0f), UIColor(0.4f, 0.5f, 0.6f, 1.0f), 2.0f, 4.0f); + drawList.AddText(UIPoint(18.0f, 22.0f), "XCUI", UIColor(1.0f, 1.0f, 1.0f, 1.0f), 16.0f); + drawList.AddImage( + UIRect(70.0f, 18.0f, 32.0f, 32.0f), + UITextureHandle{ 123u, 32u, 32u }, + UIColor(0.8f, 0.9f, 1.0f, 1.0f)); + drawList.PopClipRect(); + + ASSERT_EQ(drawList.GetCommandCount(), 6u); + const auto& commands = drawList.GetCommands(); + EXPECT_EQ(commands[0].type, UIDrawCommandType::PushClipRect); + EXPECT_EQ(commands[1].type, UIDrawCommandType::FilledRect); + EXPECT_EQ(commands[2].type, UIDrawCommandType::RectOutline); + EXPECT_EQ(commands[3].type, UIDrawCommandType::Text); + EXPECT_EQ(commands[4].type, UIDrawCommandType::Image); + EXPECT_EQ(commands[5].type, UIDrawCommandType::PopClipRect); + EXPECT_FLOAT_EQ(commands[1].rounding, 4.0f); + EXPECT_FLOAT_EQ(commands[2].thickness, 2.0f); + EXPECT_EQ(commands[3].text, "XCUI"); + EXPECT_EQ(commands[4].texture.nativeHandle, 123u); +} + +TEST(XCUIDrawDataTest, DrawDataAggregatesMultipleLists) { + using XCEngine::UI::UIColor; + using XCEngine::UI::UIDrawData; + using XCEngine::UI::UIRect; + + UIDrawData drawData = {}; + auto& first = drawData.EmplaceDrawList("First"); + first.AddFilledRect(UIRect(0.0f, 0.0f, 20.0f, 20.0f), UIColor(1.0f, 0.0f, 0.0f, 1.0f)); + + auto& second = drawData.EmplaceDrawList("Second"); + second.AddFilledRect(UIRect(5.0f, 5.0f, 10.0f, 10.0f), UIColor(0.0f, 1.0f, 0.0f, 1.0f)); + second.AddRectOutline(UIRect(5.0f, 5.0f, 10.0f, 10.0f), UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + + EXPECT_EQ(drawData.GetDrawListCount(), 2u); + EXPECT_EQ(drawData.GetTotalCommandCount(), 3u); +} diff --git a/tests/editor/test_xcui_imgui_transition_backend.cpp b/tests/editor/test_xcui_imgui_transition_backend.cpp new file mode 100644 index 00000000..40fc5579 --- /dev/null +++ b/tests/editor/test_xcui_imgui_transition_backend.cpp @@ -0,0 +1,97 @@ +#include + +#include "XCUIBackend/ImGuiTransitionBackend.h" + +#include + +#include + +namespace { + +class ImGuiContextScope { +public: + ImGuiContextScope() { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + } + + ~ImGuiContextScope() { + ImGui::DestroyContext(); + } +}; + +} // namespace + +TEST(XCUIImGuiTransitionBackendTest, BeginFrameResetsPendingState) { + ImGuiContextScope contextScope; + + XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {}; + XCEngine::UI::UIDrawList drawList("Pending"); + drawList.AddFilledRect( + XCEngine::UI::UIRect(0.0f, 0.0f, 16.0f, 16.0f), + XCEngine::UI::UIColor(1.0f, 0.0f, 0.0f, 1.0f)); + + backend.BeginFrame(); + backend.Submit(drawList); + ASSERT_EQ(backend.GetPendingDrawListCount(), 1u); + ASSERT_EQ(backend.GetPendingCommandCount(), 1u); + + backend.BeginFrame(); + EXPECT_EQ(backend.GetPendingDrawListCount(), 0u); + EXPECT_EQ(backend.GetPendingCommandCount(), 0u); +} + +TEST(XCUIImGuiTransitionBackendTest, EndFrameFlushesCommandsToImGuiDrawList) { + ImGuiContextScope contextScope; + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(1280.0f, 720.0f); + io.DeltaTime = 1.0f / 60.0f; + unsigned char* fontPixels = nullptr; + int fontWidth = 0; + int fontHeight = 0; + io.Fonts->GetTexDataAsRGBA32(&fontPixels, &fontWidth, &fontHeight); + io.Fonts->SetTexID(static_cast(1)); + + XCEngine::Editor::XCUIBackend::ImGuiTransitionBackend backend = {}; + XCEngine::UI::UIDrawList drawList("FlushTest"); + drawList.PushClipRect(XCEngine::UI::UIRect(0.0f, 0.0f, 220.0f, 160.0f)); + drawList.AddFilledRect( + XCEngine::UI::UIRect(10.0f, 10.0f, 60.0f, 40.0f), + XCEngine::UI::UIColor(0.1f, 0.2f, 0.3f, 1.0f), + 4.0f); + drawList.AddRectOutline( + XCEngine::UI::UIRect(10.0f, 10.0f, 60.0f, 40.0f), + XCEngine::UI::UIColor(0.9f, 0.9f, 0.9f, 1.0f), + 2.0f, + 4.0f); + drawList.AddText( + XCEngine::UI::UIPoint(18.0f, 26.0f), + "Backend", + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 16.0f); + drawList.AddImage( + XCEngine::UI::UIRect(90.0f, 12.0f, 32.0f, 32.0f), + XCEngine::UI::UITextureHandle{ 0x100u, 32u, 32u }, + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f)); + drawList.PopClipRect(); + + backend.BeginFrame(); + backend.Submit(drawList); + + ImGui::NewFrame(); + ASSERT_TRUE(ImGui::Begin("XCUIBackendTest")); + ImDrawList* targetDrawList = ImGui::GetWindowDrawList(); + ASSERT_NE(targetDrawList, nullptr); + + const bool flushed = backend.EndFrame(targetDrawList); + + ImGui::End(); + ImGui::EndFrame(); + + EXPECT_TRUE(flushed); + EXPECT_EQ(backend.GetLastFlushedDrawListCount(), 1u); + EXPECT_EQ(backend.GetLastFlushedCommandCount(), 6u); + EXPECT_EQ(backend.GetPendingCommandCount(), 0u); + EXPECT_GT(targetDrawList->VtxBuffer.Size, 0); + EXPECT_GT(targetDrawList->CmdBuffer.Size, 0); +}