From 551eefbaa119d5466584ec80415f266b493179fc Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 5 Apr 2026 14:05:46 +0800 Subject: [PATCH] Default new_editor to native XCUI shell host --- docs/plan/XCUI_Phase_Status_2026-04-05.md | 32 +- new_editor/src/Application.cpp | 757 ++++++++++++++++-- new_editor/src/Application.h | 26 +- .../XCUIBackend/NativeWindowUICompositor.cpp | 218 +++++ .../XCUIBackend/NativeWindowUICompositor.h | 76 ++ .../XCUIBackend/NativeXCUIPanelCanvasHost.h | 294 +++++++ .../src/XCUIBackend/XCUIInputBridge.cpp | 9 - new_editor/src/XCUIBackend/XCUIInputBridge.h | 2 - tests/NewEditor/CMakeLists.txt | 68 ++ .../test_native_window_ui_compositor.cpp | 212 +++++ .../test_native_xcui_panel_canvas_host.cpp | 176 ++++ 11 files changed, 1772 insertions(+), 98 deletions(-) create mode 100644 new_editor/src/XCUIBackend/NativeWindowUICompositor.cpp create mode 100644 new_editor/src/XCUIBackend/NativeWindowUICompositor.h create mode 100644 new_editor/src/XCUIBackend/NativeXCUIPanelCanvasHost.h create mode 100644 tests/NewEditor/test_native_window_ui_compositor.cpp create mode 100644 tests/NewEditor/test_native_xcui_panel_canvas_host.cpp diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index d3e897cb..dc91a514 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -16,6 +16,12 @@ Old `editor` replacement is explicitly out of scope for this phase. - `LayoutLab` continues as the editor widget proving ground for tree/list/property-section style controls - the demo sandbox and editor bridge APIs were tightened again without touching the old editor replacement scope - `new_editor` now has an explicit two-step window compositor seam through `IWindowUICompositor` / `ImGuiWindowUICompositor`, layered on top of `IEditorHostCompositor` / `ImGuiHostCompositor` +- The native-host follow-up is now landed in `new_editor`: + - `NativeWindowUICompositor` is now buildable alongside the legacy ImGui compositor + - `Application` now defaults to a native XCUI host path instead of creating the ImGui shell by default + - the native default path now drives `XCUIDemoRuntime` and `XCUILayoutLabRuntime` directly, composes their `UIDrawData`, and submits one swapchain packet through the native compositor + - the old ImGui shell path remains present as an explicit compatibility host instead of the default host + - `XCUIInputBridge.h` no longer drags `imgui.h` through the public XCUI input seam - Old `editor` replacement remains deferred; all active execution still stays inside XCUI shared code and `new_editor`. ## Three-Layer Status @@ -93,14 +99,24 @@ Current gap: - `XCUIDemoRuntime` now bridges pointer activation, text-edit commands, and shortcut-triggered commands through a unified command path, and `DrainPendingCommandIds()` now preserves mixed pointer/text/shortcut ordering. - `new_editor` now also has a pure `XCUIShellChromeState` model covering panel visibility, hosted-preview mode, and shell-level view toggles without depending on ImGui, `Application`, or the old editor. - `XCUIShellChromeState` hosted-preview mode naming is now backend-neutral (`HostedPresenter` / `NativeOffscreen`) instead of encoding `ImGui` into the XCUI shell model. +- `new_editor` now also has a concrete `NativeWindowUICompositor` path and native-focused compositor tests, so the window compositor seam is no longer ImGui-only. +- `Application` now also has a native XCUI shell path that: + - becomes the default `new_editor` startup path + - lays out `XCUI Demo` and `Layout Lab` as native cards directly in the swapchain window + - routes shell shortcuts through the same command router without reading ImGui capture state in the default host path + - drives both sandbox runtimes directly from Win32/XCUI input snapshots instead of routing through ImGui panel hosts + - composes one native `UIDrawData` packet and submits it through `NativeWindowUICompositor` +- `NativeXCUIPanelCanvasHost` now backs that direct shell path as an externally driven canvas/session host for native cards instead of assuming an ImGui child-window model. +- `XCUIInputBridge.h` no longer includes `imgui.h`, so the public XCUI input bridge seam is now host-neutral at the header boundary. - `XCNewEditor` builds successfully to `build/new_editor/bin/Debug/XCNewEditor.exe`. Current gap: -- The shell is still ImGui-hosted. -- Hosted-preview compatibility presentation still depends on an ImGui-specific inline draw target binding when the native queued surface path is disabled. -- The panel-canvas seam still only has an ImGui adapter today; a native panel/shell host still needs to replace it before the editor shell can stop depending on ImGui host chrome. -- Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, multi-selection/focus traversal, toolbar/menu chrome, and icon-atlas widgets are not yet extracted into reusable XCUI modules, and broader editor-host keybinding plus full shell-state adoption are still only partially integrated. +- The default shell host is now native, but the legacy ImGui shell and panel path still exists as a compatibility host and is still compiled into `new_editor`. +- Hosted-preview offscreen surface publication is still not wired for the native compositor path because descriptor-backed texture registration is still ImGui-host-centric today. +- The native shell currently proves direct runtime composition, but its shell chrome is still a bespoke `Application`-side layout rather than a fully shared XCUI-authored editor shell document. +- Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, multi-selection/focus traversal, toolbar/menu chrome, menu interaction widgets, and icon-atlas widgets are not yet extracted into reusable XCUI modules. +- Shared image support is still incomplete for full native hosted-preview parity: `UI::UIDrawData` still lacks explicit source-rect/UV-level image commands, so native surface-image composition cannot yet mirror the ImGui panel host path exactly. ## Validated This Phase @@ -113,11 +129,13 @@ Current gap: - `new_editor_xcui_rhi_render_backend_tests`: `5/5` - `new_editor_xcui_hosted_preview_presenter_tests`: `20/20` - `new_editor_imgui_window_ui_compositor_tests`: `7/7` +- `new_editor_native_window_ui_compositor_tests`: `5/5` - `new_editor_xcui_editor_command_router_tests`: `5/5` - `new_editor_application_shell_command_bindings_tests`: `6/6` - `new_editor_xcui_shell_chrome_state_tests`: `11/11` - `new_editor_xcui_panel_canvas_host_tests`: `4/4` - `new_editor_imgui_xcui_panel_canvas_host_tests`: `1/1` +- `new_editor_native_xcui_panel_canvas_host_tests`: `4/4` - `new_editor_xcui_layout_lab_panel_tests`: `3/3` - `XCNewEditor` Debug target builds successfully - `core_ui_tests`: `52 total` (`50` passed, `2` skipped because `KeyCode::Delete` currently aliases `Backspace`) @@ -212,6 +230,12 @@ Current gap: - `XCUIShellChromeState` now also exposes effective hosted-preview state helpers and shell view-toggle command-id helpers, so shell routing code no longer has to manually combine enablement and requested preview mode. - `XCUIShellChromeState` hosted-preview modes were renamed away from `LegacyImGui`, so XCUI shell state no longer treats ImGui as the generic fallback concept. - The panel-canvas seam now has dedicated null/imgui backend coverage, including explicit backend/capability reporting and a non-ImGui placeholder host path for future native shell adoption. +- The native host follow-up is now present in `new_editor`: + - `NativeWindowUICompositor` provides a swapchain-native XCUI packet present path beside the legacy ImGui compositor + - `Application` now defaults to that native host path and directly composes `XCUI Demo` plus `Layout Lab` into one native shell frame + - `NativeXCUIPanelCanvasHost` now drives externally configured native card sessions for that shell path + - new native compositor/native canvas-host tests now cover the new host seam +- `XCUIInputBridge.h` no longer includes `imgui.h`, so XCUI input translation is no longer coupled to ImGui at the public header boundary. - `SceneRuntime` layered XCUI routing now has dedicated regression coverage for: - top-interactive layer input ownership - blocking/modal layer suppression of lower layers diff --git a/new_editor/src/Application.cpp b/new_editor/src/Application.cpp index 08cdb8fc..eb471f38 100644 --- a/new_editor/src/Application.cpp +++ b/new_editor/src/Application.cpp @@ -2,13 +2,16 @@ #include "XCUIBackend/ImGuiXCUIPanelCanvasHost.h" #include "XCUIBackend/ImGuiXCUIHostedPreviewPresenter.h" #include "XCUIBackend/ImGuiWindowUICompositor.h" +#include "XCUIBackend/NativeWindowUICompositor.h" #include +#include #include #include #include +#include namespace XCEngine { namespace NewEditor { @@ -18,6 +21,14 @@ constexpr wchar_t kWindowClassName[] = L"XCNewEditorWindowClass"; constexpr wchar_t kWindowTitle[] = L"XCNewEditor"; constexpr float kClearColor[4] = { 0.08f, 0.09f, 0.11f, 1.0f }; constexpr float kHostedPreviewClearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; +constexpr float kNativeShellOuterMargin = 22.0f; +constexpr float kNativeShellInnerGap = 18.0f; +constexpr float kNativeShellHeaderHeight = 58.0f; +constexpr float kNativeShellFooterHeight = 34.0f; +constexpr float kNativePanelHeaderHeight = 42.0f; +constexpr float kNativePanelPadding = 14.0f; +constexpr float kNativePanelMinWidth = 260.0f; +constexpr float kNativePanelMinHeight = 180.0f; template void ShutdownAndDelete(ResourceType*& resource) { @@ -78,6 +89,100 @@ const char* GetHostedPreviewStateLabel( } return "idle"; } + +bool ContainsPoint(const UI::UIRect& rect, const UI::UIPoint& point) { + return point.x >= rect.x && + point.y >= rect.y && + point.x <= rect.x + rect.width && + point.y <= rect.y + rect.height; +} + +std::uint64_t MakeFrameTimestampNanoseconds() { + return static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); +} + +void AppendDrawData(::XCEngine::UI::UIDrawData& destination, const ::XCEngine::UI::UIDrawData& source) { + for (const ::XCEngine::UI::UIDrawList& drawList : source.GetDrawLists()) { + destination.AddDrawList(drawList); + } +} + +bool ContainsKeyTransition( + const std::vector& keys, + std::int32_t keyCode) { + return std::find(keys.begin(), keys.end(), keyCode) != keys.end(); +} + +bool ShouldCaptureKeyboardNavigation( + const ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession& canvasSession, + const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& previousFrame) { + if (!canvasSession.validCanvas) { + return false; + } + + return canvasSession.hovered || + (canvasSession.windowFocused && + !previousFrame.stats.selectedElementId.empty()); +} + +void PopulateKeyboardNavigationInput( + ::XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState& input, + const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta& frameDelta, + bool captureKeyboardNavigation) { + if (!captureKeyboardNavigation) { + return; + } + + using ::XCEngine::Input::KeyCode; + const auto pressedThisFrame = + [&frameDelta](KeyCode keyCode) { + const std::int32_t code = static_cast(keyCode); + return ContainsKeyTransition(frameDelta.keyboard.pressedKeys, code) || + ContainsKeyTransition(frameDelta.keyboard.repeatedKeys, code); + }; + + input.navigatePrevious = pressedThisFrame(KeyCode::Up); + input.navigateNext = pressedThisFrame(KeyCode::Down); + input.navigateHome = pressedThisFrame(KeyCode::Home); + input.navigateEnd = pressedThisFrame(KeyCode::End); + input.navigateCollapse = pressedThisFrame(KeyCode::Left); + input.navigateExpand = pressedThisFrame(KeyCode::Right); +} + +struct NativeShellPanelLayout { + Application::ShellPanelId panelId = Application::ShellPanelId::XCUIDemo; + std::string title = {}; + UI::UIRect panelRect = {}; + UI::UIRect canvasRect = {}; + bool visible = false; + bool hovered = false; + bool active = false; +}; + +NativeShellPanelLayout MakePanelLayout( + Application::ShellPanelId panelId, + std::string title, + const UI::UIRect& panelRect, + const UI::UIPoint& pointerPosition, + bool windowFocused, + bool active) { + NativeShellPanelLayout layout = {}; + layout.panelId = panelId; + layout.title = std::move(title); + layout.panelRect = panelRect; + layout.canvasRect = UI::UIRect( + panelRect.x + kNativePanelPadding, + panelRect.y + kNativePanelHeaderHeight, + (std::max)(0.0f, panelRect.width - kNativePanelPadding * 2.0f), + (std::max)(0.0f, panelRect.height - kNativePanelHeaderHeight - kNativePanelPadding)); + layout.visible = panelRect.width >= kNativePanelMinWidth && panelRect.height >= kNativePanelMinHeight; + layout.hovered = windowFocused && ContainsPoint(layout.canvasRect, pointerPosition); + layout.active = active; + return layout; +} } // namespace std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> @@ -91,6 +196,22 @@ Application::CreateHostedPreviewPresenter(bool nativePreview) { return ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter(); } +bool Application::IsNativeWindowHostEnabled() const { + return m_windowHostMode == WindowHostMode::NativeXCUI; +} + +void Application::InitializeNativeShell() { + m_nativeActivePanel = m_shellChromeState.IsPanelVisible(ShellPanelId::XCUIDemo) + ? ShellPanelId::XCUIDemo + : ShellPanelId::XCUILayoutLab; + m_nativeDemoInputBridge.Reset(); + m_nativeLayoutInputBridge.Reset(); + m_nativeDemoCanvasHost.ClearCanvasSession(); + m_nativeLayoutCanvasHost.ClearCanvasSession(); + m_nativeDemoReloadSucceeded = m_nativeDemoRuntime.ReloadDocuments(); + m_nativeLayoutReloadSucceeded = m_nativeLayoutRuntime.ReloadDocuments(); +} + const Application::ShellPanelChromeState* Application::TryGetShellPanelState(ShellPanelId panelId) const { return m_shellChromeState.TryGetPanelState(panelId); } @@ -197,20 +318,25 @@ void Application::ConfigureShellCommandRouter() { Application::RegisterShellViewCommands(m_shellCommandRouter, bindings); } -void Application::DispatchShellShortcuts() { - ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeCaptureOptions options = {}; - ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot snapshot = - m_xcuiInputSource.CaptureSnapshot(options); - - ImGuiIO& io = ImGui::GetIO(); - snapshot.wantCaptureKeyboard = io.WantCaptureKeyboard; - snapshot.wantTextInput = io.WantTextInput; +::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta +Application::DispatchShellShortcuts( + const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot& snapshot) { + ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot shellSnapshot = snapshot; + if (!IsNativeWindowHostEnabled()) { + ImGuiIO& io = ImGui::GetIO(); + shellSnapshot.wantCaptureKeyboard = io.WantCaptureKeyboard; + shellSnapshot.wantTextInput = io.WantTextInput; + } + if (!m_shellInputBridge.HasBaseline()) { + m_shellInputBridge.Prime(shellSnapshot); + } const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta frameDelta = - m_shellInputBridge.Translate(snapshot); + m_shellInputBridge.Translate(shellSnapshot); const ::XCEngine::Editor::XCUIBackend::XCUIEditorCommandInputSnapshot commandSnapshot = Application::BuildShellShortcutSnapshot(frameDelta); m_shellCommandRouter.InvokeMatchingShortcut({ &commandSnapshot }); + return frameDelta; } Application::HostedPreviewPanelDiagnostics Application::BuildHostedPreviewPanelDiagnostics( @@ -293,15 +419,19 @@ int Application::Run(HINSTANCE instance, int nCmdShow) { } InitializeWindowCompositor(); - m_demoPanel = std::make_unique( - &m_xcuiInputSource, - CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo)), - ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIPanelCanvasHost()); - m_layoutLabPanel = std::make_unique( - &m_xcuiInputSource, - CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab)), - ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIPanelCanvasHost()); - ConfigureHostedPreviewPresenters(); + if (IsNativeWindowHostEnabled()) { + InitializeNativeShell(); + } else { + m_demoPanel = std::make_unique( + &m_xcuiInputSource, + CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo)), + ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIPanelCanvasHost()); + m_layoutLabPanel = std::make_unique( + &m_xcuiInputSource, + CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab)), + ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIPanelCanvasHost()); + ConfigureHostedPreviewPresenters(); + } m_shellInputBridge.Reset(); ConfigureShellCommandRouter(); m_running = true; @@ -433,7 +563,9 @@ bool Application::InitializeRenderer() { } void Application::InitializeWindowCompositor() { - m_windowCompositor = ::XCEngine::Editor::XCUIBackend::CreateImGuiWindowUICompositor(); + m_windowCompositor = IsNativeWindowHostEnabled() + ? ::XCEngine::Editor::XCUIBackend::CreateNativeWindowUICompositor() + : ::XCEngine::Editor::XCUIBackend::CreateImGuiWindowUICompositor(); if (m_windowCompositor != nullptr) { m_windowCompositor->Initialize( m_hwnd, @@ -654,6 +786,525 @@ bool Application::RenderHostedPreviewOffscreenSurface( return true; } +::XCEngine::UI::UIDrawData Application::BuildNativeShellDrawData( + const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot& shellSnapshot, + const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta& shellFrameDelta) { + ::XCEngine::UI::UIDrawData composedDrawData = {}; + + RECT clientRect = {}; + if (!GetClientRect(m_hwnd, &clientRect)) { + return composedDrawData; + } + + const float windowWidth = static_cast(clientRect.right - clientRect.left); + const float windowHeight = static_cast(clientRect.bottom - clientRect.top); + if (windowWidth <= 1.0f || windowHeight <= 1.0f) { + return composedDrawData; + } + + const UI::UIRect shellRect(0.0f, 0.0f, windowWidth, windowHeight); + const UI::UIColor shellBorderColor(40.0f / 255.0f, 54.0f / 255.0f, 74.0f / 255.0f, 1.0f); + const UI::UIColor shellSurfaceColor(11.0f / 255.0f, 15.0f / 255.0f, 22.0f / 255.0f, 180.0f / 255.0f); + const UI::UIColor panelSurfaceColor(9.0f / 255.0f, 13.0f / 255.0f, 18.0f / 255.0f, 212.0f / 255.0f); + const UI::UIColor panelBorderColor(53.0f / 255.0f, 72.0f / 255.0f, 96.0f / 255.0f, 1.0f); + const UI::UIColor panelAccentColor(84.0f / 255.0f, 176.0f / 255.0f, 244.0f / 255.0f, 1.0f); + const UI::UIColor hoveredAccentColor(1.0f, 206.0f / 255.0f, 112.0f / 255.0f, 1.0f); + const UI::UIColor textPrimary(232.0f / 255.0f, 238.0f / 255.0f, 246.0f / 255.0f, 1.0f); + const UI::UIColor textSecondary(150.0f / 255.0f, 164.0f / 255.0f, 184.0f / 255.0f, 1.0f); + const UI::UIColor textMuted(108.0f / 255.0f, 123.0f / 255.0f, 145.0f / 255.0f, 1.0f); + + const float topBarY = kNativeShellOuterMargin; + const UI::UIRect topBarRect( + kNativeShellOuterMargin, + topBarY, + (std::max)(0.0f, windowWidth - kNativeShellOuterMargin * 2.0f), + kNativeShellHeaderHeight); + const UI::UIRect footerRect( + kNativeShellOuterMargin, + (std::max)(topBarRect.y + topBarRect.height + kNativeShellInnerGap, windowHeight - kNativeShellOuterMargin - kNativeShellFooterHeight), + (std::max)(0.0f, windowWidth - kNativeShellOuterMargin * 2.0f), + kNativeShellFooterHeight); + + const float workspaceTop = topBarRect.y + topBarRect.height + kNativeShellInnerGap; + const float workspaceBottom = footerRect.y - kNativeShellInnerGap; + const UI::UIRect workspaceRect( + kNativeShellOuterMargin, + workspaceTop, + (std::max)(0.0f, windowWidth - kNativeShellOuterMargin * 2.0f), + (std::max)(0.0f, workspaceBottom - workspaceTop)); + + bool demoVisible = m_shellChromeState.IsPanelVisible(ShellPanelId::XCUIDemo); + bool layoutVisible = m_shellChromeState.IsPanelVisible(ShellPanelId::XCUILayoutLab); + if (m_nativeActivePanel == ShellPanelId::XCUIDemo && !demoVisible && layoutVisible) { + m_nativeActivePanel = ShellPanelId::XCUILayoutLab; + } else if (m_nativeActivePanel == ShellPanelId::XCUILayoutLab && !layoutVisible && demoVisible) { + m_nativeActivePanel = ShellPanelId::XCUIDemo; + } + + std::vector panelLayouts = {}; + panelLayouts.reserve(2u); + if (demoVisible && layoutVisible) { + const float leftWidth = (std::max)( + kNativePanelMinWidth, + (std::min)(workspaceRect.width * 0.60f, workspaceRect.width - kNativePanelMinWidth - kNativeShellInnerGap)); + const float rightWidth = (std::max)(0.0f, workspaceRect.width - leftWidth - kNativeShellInnerGap); + panelLayouts.push_back(MakePanelLayout( + ShellPanelId::XCUIDemo, + "XCUI Demo", + UI::UIRect(workspaceRect.x, workspaceRect.y, leftWidth, workspaceRect.height), + shellSnapshot.pointerPosition, + shellSnapshot.windowFocused, + m_nativeActivePanel == ShellPanelId::XCUIDemo)); + panelLayouts.push_back(MakePanelLayout( + ShellPanelId::XCUILayoutLab, + "XCUI Layout Lab", + UI::UIRect(workspaceRect.x + leftWidth + kNativeShellInnerGap, workspaceRect.y, rightWidth, workspaceRect.height), + shellSnapshot.pointerPosition, + shellSnapshot.windowFocused, + m_nativeActivePanel == ShellPanelId::XCUILayoutLab)); + } else if (demoVisible) { + panelLayouts.push_back(MakePanelLayout( + ShellPanelId::XCUIDemo, + "XCUI Demo", + workspaceRect, + shellSnapshot.pointerPosition, + shellSnapshot.windowFocused, + true)); + m_nativeActivePanel = ShellPanelId::XCUIDemo; + } else if (layoutVisible) { + panelLayouts.push_back(MakePanelLayout( + ShellPanelId::XCUILayoutLab, + "XCUI Layout Lab", + workspaceRect, + shellSnapshot.pointerPosition, + shellSnapshot.windowFocused, + true)); + m_nativeActivePanel = ShellPanelId::XCUILayoutLab; + } + + if (shellFrameDelta.pointer.pressed[0]) { + for (const NativeShellPanelLayout& panelLayout : panelLayouts) { + if (panelLayout.hovered) { + m_nativeActivePanel = panelLayout.panelId; + break; + } + } + for (NativeShellPanelLayout& panelLayout : panelLayouts) { + panelLayout.active = panelLayout.panelId == m_nativeActivePanel; + } + } + + ::XCEngine::UI::UIDrawList& chromeBackground = + composedDrawData.EmplaceDrawList("XCUI.NativeShell.Background"); + chromeBackground.AddFilledRect(shellRect, shellSurfaceColor, 0.0f); + chromeBackground.AddFilledRect(topBarRect, UI::UIColor(12.0f / 255.0f, 18.0f / 255.0f, 26.0f / 255.0f, 230.0f / 255.0f), 14.0f); + chromeBackground.AddRectOutline(topBarRect, shellBorderColor, 1.0f, 14.0f); + chromeBackground.AddFilledRect(footerRect, UI::UIColor(12.0f / 255.0f, 18.0f / 255.0f, 26.0f / 255.0f, 214.0f / 255.0f), 12.0f); + chromeBackground.AddRectOutline(footerRect, shellBorderColor, 1.0f, 12.0f); + + if (panelLayouts.empty()) { + const UI::UIRect emptyStateRect( + workspaceRect.x, + workspaceRect.y, + workspaceRect.width, + (std::max)(0.0f, workspaceRect.height)); + chromeBackground.AddFilledRect(emptyStateRect, panelSurfaceColor, 18.0f); + chromeBackground.AddRectOutline(emptyStateRect, panelBorderColor, 1.0f, 18.0f); + + ::XCEngine::UI::UIDrawList& emptyForeground = + composedDrawData.EmplaceDrawList("XCUI.NativeShell.EmptyState"); + emptyForeground.AddText( + UI::UIPoint(emptyStateRect.x + 24.0f, emptyStateRect.y + 28.0f), + "XCUI native shell is active, but both sandbox panels are hidden.", + textPrimary); + emptyForeground.AddText( + UI::UIPoint(emptyStateRect.x + 24.0f, emptyStateRect.y + 50.0f), + "Use Ctrl+1 or Ctrl+2 to bring a panel back.", + textSecondary); + return composedDrawData; + } + + for (const NativeShellPanelLayout& panelLayout : panelLayouts) { + const UI::UIColor borderColor = panelLayout.active + ? panelAccentColor + : (panelLayout.hovered ? hoveredAccentColor : panelBorderColor); + chromeBackground.AddFilledRect(panelLayout.panelRect, panelSurfaceColor, 18.0f); + chromeBackground.AddRectOutline(panelLayout.panelRect, borderColor, panelLayout.active ? 2.0f : 1.0f, 18.0f); + chromeBackground.AddFilledRect( + UI::UIRect( + panelLayout.panelRect.x, + panelLayout.panelRect.y, + panelLayout.panelRect.width, + kNativePanelHeaderHeight), + UI::UIColor(13.0f / 255.0f, 20.0f / 255.0f, 28.0f / 255.0f, 242.0f / 255.0f), + 18.0f); + } + + struct NativePanelFrameSummary { + NativeShellPanelLayout layout = {}; + std::string lineA = {}; + std::string lineB = {}; + ::XCEngine::UI::UIDrawData overlay = {}; + }; + + std::vector panelSummaries = {}; + panelSummaries.reserve(panelLayouts.size()); + + const auto capturePanelSnapshot = + [this, &shellSnapshot](const NativeShellPanelLayout& panelLayout, bool wantsKeyboard) { + ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeCaptureOptions options = {}; + options.timestampNanoseconds = shellSnapshot.timestampNanoseconds; + options.windowFocused = shellSnapshot.windowFocused; + options.hasPointerInsideOverride = true; + options.pointerInsideOverride = panelLayout.hovered; + + ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot panelSnapshot = + m_xcuiInputSource.CaptureSnapshot(options); + if (!wantsKeyboard) { + panelSnapshot.keys.clear(); + panelSnapshot.characters.clear(); + panelSnapshot.wantCaptureKeyboard = false; + panelSnapshot.wantTextInput = false; + } + return panelSnapshot; + }; + + const auto extractCanvasOverlay = + [](const ::XCEngine::Editor::XCUIBackend::NativeXCUIPanelCanvasHost& canvasHost) { + ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasFrameSnapshot snapshot = {}; + if (!canvasHost.TryGetLatestFrameSnapshot(snapshot)) { + return ::XCEngine::UI::UIDrawData(); + } + return snapshot.overlayDrawData; + }; + + for (const NativeShellPanelLayout& panelLayout : panelLayouts) { + if (panelLayout.panelId == ShellPanelId::XCUIDemo) { + ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession canvasSession = {}; + canvasSession.hostRect = panelLayout.panelRect; + canvasSession.canvasRect = panelLayout.canvasRect; + canvasSession.pointerPosition = shellSnapshot.pointerPosition; + canvasSession.validCanvas = panelLayout.canvasRect.width > 1.0f && panelLayout.canvasRect.height > 1.0f; + canvasSession.hovered = panelLayout.hovered; + canvasSession.windowFocused = shellSnapshot.windowFocused; + m_nativeDemoCanvasHost.SetCanvasSession(canvasSession); + + ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasRequest canvasRequest = {}; + canvasRequest.childId = "XCUIDemo.NativeCanvas"; + canvasRequest.height = panelLayout.panelRect.height; + canvasRequest.topInset = panelLayout.canvasRect.y - panelLayout.panelRect.y; + canvasRequest.drawPreviewFrame = false; + const ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession resolvedSession = + m_nativeDemoCanvasHost.BeginCanvas(canvasRequest); + + const bool wantsKeyboard = panelLayout.active; + const auto panelSnapshot = capturePanelSnapshot(panelLayout, wantsKeyboard); + if (!m_nativeDemoInputBridge.HasBaseline()) { + m_nativeDemoInputBridge.Prime(panelSnapshot); + } + const auto panelFrameDelta = m_nativeDemoInputBridge.Translate(panelSnapshot); + + ::XCEngine::Editor::XCUIBackend::XCUIDemoInputState input = {}; + input.canvasRect = resolvedSession.canvasRect; + input.pointerPosition = panelSnapshot.pointerPosition; + input.pointerInside = panelSnapshot.pointerInside; + input.pointerPressed = panelFrameDelta.pointer.pressed[0]; + input.pointerReleased = panelFrameDelta.pointer.released[0]; + input.pointerDown = panelSnapshot.pointerButtonsDown[0]; + input.windowFocused = panelSnapshot.windowFocused; + input.wantCaptureMouse = panelSnapshot.wantCaptureMouse; + input.wantCaptureKeyboard = panelSnapshot.wantCaptureKeyboard; + input.wantTextInput = panelSnapshot.wantTextInput; + input.events = panelFrameDelta.events; + + const auto& frame = m_nativeDemoRuntime.Update(input); + AppendDrawData(composedDrawData, frame.drawData); + + if (IsShellViewToggleEnabled(ShellViewToggleId::HostedPreviewHud)) { + const auto& stats = frame.stats; + const UI::UIRect hudRect( + resolvedSession.canvasRect.x + 10.0f, + resolvedSession.canvasRect.y + 10.0f, + (std::min)(resolvedSession.canvasRect.width - 20.0f, 360.0f), + 62.0f); + if (hudRect.width > 40.0f && hudRect.height > 20.0f) { + m_nativeDemoCanvasHost.DrawFilledRect( + hudRect, + UI::UIColor(12.0f / 255.0f, 18.0f / 255.0f, 26.0f / 255.0f, 214.0f / 255.0f), + 10.0f); + m_nativeDemoCanvasHost.DrawOutlineRect(hudRect, panelBorderColor, 1.0f, 10.0f); + m_nativeDemoCanvasHost.DrawText( + UI::UIPoint(hudRect.x + 10.0f, hudRect.y + 8.0f), + "Direct native XCUI frame", + textPrimary); + m_nativeDemoCanvasHost.DrawText( + UI::UIPoint(hudRect.x + 10.0f, hudRect.y + 28.0f), + std::string("Tree ") + std::to_string(static_cast(stats.treeGeneration)) + + " | Elements " + std::to_string(stats.elementCount) + + " | Commands " + std::to_string(stats.commandCount), + textSecondary); + m_nativeDemoCanvasHost.DrawText( + UI::UIPoint(hudRect.x + 10.0f, hudRect.y + 46.0f), + stats.statusMessage, + textMuted); + } + + const auto drawDebugRect = + [this, &stats](const std::string& elementId, const UI::UIColor& color, const char* label) { + if (elementId.empty()) { + return; + } + UI::UIRect rect = {}; + if (!m_nativeDemoRuntime.TryGetElementRect(elementId, rect)) { + return; + } + m_nativeDemoCanvasHost.DrawOutlineRect(rect, color, 2.0f, 6.0f); + if (label != nullptr && label[0] != '\0') { + m_nativeDemoCanvasHost.DrawText(UI::UIPoint(rect.x + 4.0f, rect.y + 4.0f), label, color); + } + }; + drawDebugRect(stats.hoveredElementId, UI::UIColor(1.0f, 195.0f / 255.0f, 64.0f / 255.0f, 1.0f), "hover"); + drawDebugRect(stats.focusedElementId, UI::UIColor(64.0f / 255.0f, 214.0f / 255.0f, 1.0f, 1.0f), "focus"); + } + + m_nativeDemoCanvasHost.EndCanvas(); + + NativePanelFrameSummary summary = {}; + summary.layout = panelLayout; + summary.lineA = m_nativeDemoReloadSucceeded + ? frame.stats.statusMessage + : "Document reload failed; showing last retained runtime state."; + summary.lineB = + std::string(panelLayout.active ? "Active" : "Passive") + + " | " + std::to_string(frame.stats.elementCount) + + " elements | " + std::to_string(frame.stats.commandCount) + + " cmds"; + summary.overlay = extractCanvasOverlay(m_nativeDemoCanvasHost); + panelSummaries.push_back(std::move(summary)); + m_nativeDemoCanvasHost.ClearCanvasSession(); + continue; + } + + ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession canvasSession = {}; + canvasSession.hostRect = panelLayout.panelRect; + canvasSession.canvasRect = panelLayout.canvasRect; + canvasSession.pointerPosition = shellSnapshot.pointerPosition; + canvasSession.validCanvas = panelLayout.canvasRect.width > 1.0f && panelLayout.canvasRect.height > 1.0f; + canvasSession.hovered = panelLayout.hovered; + canvasSession.windowFocused = shellSnapshot.windowFocused; + m_nativeLayoutCanvasHost.SetCanvasSession(canvasSession); + + ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasRequest canvasRequest = {}; + canvasRequest.childId = "XCUILayoutLab.NativeCanvas"; + canvasRequest.height = panelLayout.panelRect.height; + canvasRequest.topInset = panelLayout.canvasRect.y - panelLayout.panelRect.y; + canvasRequest.drawPreviewFrame = false; + const auto resolvedSession = m_nativeLayoutCanvasHost.BeginCanvas(canvasRequest); + + const bool wantsKeyboard = panelLayout.active; + const auto panelSnapshot = capturePanelSnapshot(panelLayout, wantsKeyboard); + if (!m_nativeLayoutInputBridge.HasBaseline()) { + m_nativeLayoutInputBridge.Prime(panelSnapshot); + } + const auto panelFrameDelta = m_nativeLayoutInputBridge.Translate(panelSnapshot); + + ::XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState input = {}; + input.canvasRect = resolvedSession.canvasRect; + input.pointerPosition = panelSnapshot.pointerPosition; + input.pointerInside = panelSnapshot.pointerInside; + input.pointerPressed = input.pointerInside && panelFrameDelta.pointer.pressed[0]; + PopulateKeyboardNavigationInput( + input, + panelFrameDelta, + panelLayout.active && ShouldCaptureKeyboardNavigation(resolvedSession, m_nativeLayoutRuntime.GetFrameResult())); + + const auto& frame = m_nativeLayoutRuntime.Update(input); + AppendDrawData(composedDrawData, frame.drawData); + m_nativeLayoutCanvasHost.EndCanvas(); + + NativePanelFrameSummary summary = {}; + summary.layout = panelLayout; + summary.lineA = m_nativeLayoutReloadSucceeded + ? frame.stats.statusMessage + : "Layout lab reload failed; showing last retained runtime state."; + summary.lineB = + std::to_string(frame.stats.rowCount) + " rows | " + + std::to_string(frame.stats.columnCount) + " cols | " + + std::to_string(frame.stats.commandCount) + " cmds"; + summary.overlay = extractCanvasOverlay(m_nativeLayoutCanvasHost); + panelSummaries.push_back(std::move(summary)); + m_nativeLayoutCanvasHost.ClearCanvasSession(); + } + + ::XCEngine::UI::UIDrawList& chromeForeground = + composedDrawData.EmplaceDrawList("XCUI.NativeShell.Foreground"); + chromeForeground.AddText( + UI::UIPoint(topBarRect.x + 18.0f, topBarRect.y + 14.0f), + "XCUI Native Shell", + textPrimary); + chromeForeground.AddText( + UI::UIPoint(topBarRect.x + 18.0f, topBarRect.y + 34.0f), + "Default host path is now direct XCUI composition over the swapchain, with ImGui kept only as an explicit compatibility shell.", + textSecondary); + + std::ostringstream footerStream = {}; + footerStream + << "Ctrl+1 Demo | Ctrl+2 Layout | Ctrl+Shift+B Backdrop " + << "| Ctrl+Shift+H HUD | Active panel: " + << (m_nativeActivePanel == ShellPanelId::XCUIDemo ? "XCUI Demo" : "XCUI Layout Lab"); + chromeForeground.AddText( + UI::UIPoint(footerRect.x + 14.0f, footerRect.y + 10.0f), + footerStream.str(), + textSecondary); + + for (const NativePanelFrameSummary& summary : panelSummaries) { + chromeForeground.AddText( + UI::UIPoint(summary.layout.panelRect.x + 16.0f, summary.layout.panelRect.y + 12.0f), + summary.layout.title, + textPrimary); + chromeForeground.AddText( + UI::UIPoint(summary.layout.panelRect.x + 16.0f, summary.layout.panelRect.y + 28.0f), + summary.lineA, + textSecondary); + chromeForeground.AddText( + UI::UIPoint(summary.layout.panelRect.x + 16.0f, summary.layout.panelRect.y + summary.layout.panelRect.height - 18.0f), + summary.lineB, + textMuted); + } + + if (IsShellViewToggleEnabled(ShellViewToggleId::NativeXCUIOverlay)) { + const UI::UIRect overlayRect( + topBarRect.x + topBarRect.width - 282.0f, + topBarRect.y + 10.0f, + 266.0f, + 38.0f); + chromeForeground.AddFilledRect( + overlayRect, + UI::UIColor(18.0f / 255.0f, 72.0f / 255.0f, 112.0f / 255.0f, 196.0f / 255.0f), + 10.0f); + chromeForeground.AddRectOutline(overlayRect, panelAccentColor, 1.0f, 10.0f); + chromeForeground.AddText( + UI::UIPoint(overlayRect.x + 12.0f, overlayRect.y + 12.0f), + "Overlay: native compositor + direct XCUI packet", + textPrimary); + } + + for (const NativePanelFrameSummary& summary : panelSummaries) { + AppendDrawData(composedDrawData, summary.overlay); + } + + return composedDrawData; +} + +void Application::FrameLegacyImGuiHost() { + m_hostedPreviewQueue.BeginFrame(); + m_hostedPreviewSurfaceRegistry.BeginFrame(); + SyncHostedPreviewSurfaces(); + if (m_windowCompositor == nullptr) { + m_xcuiInputSource.ClearFrameTransients(); + return; + } + + m_windowCompositor->RenderFrame( + kClearColor, + [this]() { + ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeCaptureOptions options = {}; + options.timestampNanoseconds = MakeFrameTimestampNanoseconds(); + options.windowFocused = m_xcuiInputSource.IsWindowFocused(); + const auto shellSnapshot = m_xcuiInputSource.CaptureSnapshot(options); + DispatchShellShortcuts(shellSnapshot); + RenderShellChrome(); + if (m_demoPanel) { + m_demoPanel->RenderIfVisible(); + } + if (m_layoutLabPanel) { + m_layoutLabPanel->RenderIfVisible(); + } + bool showImGuiDemoWindow = IsShellViewToggleEnabled(ShellViewToggleId::ImGuiDemoWindow); + if (showImGuiDemoWindow) { + ImGui::ShowDemoWindow(&showImGuiDemoWindow); + SetShellViewToggleEnabled(ShellViewToggleId::ImGuiDemoWindow, showImGuiDemoWindow); + } + + SyncShellChromePanelStateFromPanels(); + SyncHostedPreviewSurfaces(); + }, + [this]( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface) { + RenderQueuedHostedPreviews(renderContext, surface); + + if (!IsShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop) && + !IsShellViewToggleEnabled(ShellViewToggleId::NativeXCUIOverlay)) { + return; + } + + MainWindowNativeBackdropRenderer::FrameState frameState = {}; + frameState.elapsedSeconds = static_cast( + std::chrono::duration(std::chrono::steady_clock::now() - m_startTime).count()); + frameState.pulseAccent = IsShellViewToggleEnabled(ShellViewToggleId::PulseAccent); + frameState.drawBackdrop = IsShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop); + if (IsShellViewToggleEnabled(ShellViewToggleId::NativeXCUIOverlay)) { + const float width = static_cast(surface.GetWidth()); + const float height = static_cast(surface.GetHeight()); + const float horizontalMargin = (std::min)(width * 0.14f, 128.0f); + const float topMargin = (std::min)(height * 0.15f, 132.0f); + const float bottomMargin = (std::min)(height * 0.12f, 96.0f); + + ::XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState overlayInput = {}; + overlayInput.canvasRect = ::XCEngine::UI::UIRect( + horizontalMargin, + topMargin, + (std::max)(0.0f, width - horizontalMargin * 2.0f), + (std::max)(0.0f, height - topMargin - bottomMargin)); + overlayInput.pointerPosition = m_xcuiInputSource.GetPointerPosition(); + overlayInput.pointerInside = + overlayInput.pointerPosition.x >= overlayInput.canvasRect.x && + overlayInput.pointerPosition.y >= overlayInput.canvasRect.y && + overlayInput.pointerPosition.x <= overlayInput.canvasRect.x + overlayInput.canvasRect.width && + overlayInput.pointerPosition.y <= overlayInput.canvasRect.y + overlayInput.canvasRect.height; + const auto& overlayFrame = m_nativeOverlayRuntime.Update(overlayInput); + frameState.overlayDrawData = &overlayFrame.drawData; + } + m_nativeBackdropRenderer.Render(renderContext, surface, frameState); + }); + + m_xcuiInputSource.ClearFrameTransients(); +} + +void Application::FrameNativeXCUIHost() { + auto* nativeCompositor = + dynamic_cast<::XCEngine::Editor::XCUIBackend::NativeWindowUICompositor*>(m_windowCompositor.get()); + if (nativeCompositor == nullptr) { + m_xcuiInputSource.ClearFrameTransients(); + return; + } + + ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeCaptureOptions options = {}; + options.timestampNanoseconds = MakeFrameTimestampNanoseconds(); + options.windowFocused = m_xcuiInputSource.IsWindowFocused(); + const auto shellSnapshot = m_xcuiInputSource.CaptureSnapshot(options); + const auto shellFrameDelta = DispatchShellShortcuts(shellSnapshot); + ::XCEngine::UI::UIDrawData nativeShellDrawData = BuildNativeShellDrawData(shellSnapshot, shellFrameDelta); + nativeCompositor->SubmitRenderPacket(nativeShellDrawData, &m_hostedPreviewTextAtlasProvider); + + m_windowCompositor->RenderFrame( + kClearColor, + {}, + [this]( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface) { + MainWindowNativeBackdropRenderer::FrameState frameState = {}; + frameState.elapsedSeconds = static_cast( + std::chrono::duration(std::chrono::steady_clock::now() - m_startTime).count()); + frameState.pulseAccent = IsShellViewToggleEnabled(ShellViewToggleId::PulseAccent); + frameState.drawBackdrop = IsShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop); + m_nativeBackdropRenderer.Render(renderContext, surface, frameState); + }); + + m_xcuiInputSource.ClearFrameTransients(); +} + void Application::RenderShellChrome() { SyncShellChromePanelStateFromPanels(); @@ -1023,75 +1674,17 @@ void Application::Frame() { return; } - m_hostedPreviewQueue.BeginFrame(); - m_hostedPreviewSurfaceRegistry.BeginFrame(); - SyncHostedPreviewSurfaces(); if (m_windowCompositor == nullptr) { m_xcuiInputSource.ClearFrameTransients(); return; } - m_windowCompositor->RenderFrame( - kClearColor, - [this]() { - DispatchShellShortcuts(); - RenderShellChrome(); - if (m_demoPanel) { - m_demoPanel->RenderIfVisible(); - } - if (m_layoutLabPanel) { - m_layoutLabPanel->RenderIfVisible(); - } - bool showImGuiDemoWindow = IsShellViewToggleEnabled(ShellViewToggleId::ImGuiDemoWindow); - if (showImGuiDemoWindow) { - ImGui::ShowDemoWindow(&showImGuiDemoWindow); - SetShellViewToggleEnabled(ShellViewToggleId::ImGuiDemoWindow, showImGuiDemoWindow); - } + if (IsNativeWindowHostEnabled()) { + FrameNativeXCUIHost(); + return; + } - SyncShellChromePanelStateFromPanels(); - SyncHostedPreviewSurfaces(); - }, - [this]( - const ::XCEngine::Rendering::RenderContext& renderContext, - const ::XCEngine::Rendering::RenderSurface& surface) { - RenderQueuedHostedPreviews(renderContext, surface); - - if (!IsShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop) && - !IsShellViewToggleEnabled(ShellViewToggleId::NativeXCUIOverlay)) { - return; - } - - MainWindowNativeBackdropRenderer::FrameState frameState = {}; - frameState.elapsedSeconds = static_cast( - std::chrono::duration(std::chrono::steady_clock::now() - m_startTime).count()); - frameState.pulseAccent = IsShellViewToggleEnabled(ShellViewToggleId::PulseAccent); - frameState.drawBackdrop = IsShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop); - if (IsShellViewToggleEnabled(ShellViewToggleId::NativeXCUIOverlay)) { - const float width = static_cast(surface.GetWidth()); - const float height = static_cast(surface.GetHeight()); - const float horizontalMargin = (std::min)(width * 0.14f, 128.0f); - const float topMargin = (std::min)(height * 0.15f, 132.0f); - const float bottomMargin = (std::min)(height * 0.12f, 96.0f); - - ::XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState overlayInput = {}; - overlayInput.canvasRect = ::XCEngine::UI::UIRect( - horizontalMargin, - topMargin, - (std::max)(0.0f, width - horizontalMargin * 2.0f), - (std::max)(0.0f, height - topMargin - bottomMargin)); - overlayInput.pointerPosition = m_xcuiInputSource.GetPointerPosition(); - overlayInput.pointerInside = - overlayInput.pointerPosition.x >= overlayInput.canvasRect.x && - overlayInput.pointerPosition.y >= overlayInput.canvasRect.y && - overlayInput.pointerPosition.x <= overlayInput.canvasRect.x + overlayInput.canvasRect.width && - overlayInput.pointerPosition.y <= overlayInput.canvasRect.y + overlayInput.canvasRect.height; - const auto& overlayFrame = m_nativeOverlayRuntime.Update(overlayInput); - frameState.overlayDrawData = &overlayFrame.drawData; - } - m_nativeBackdropRenderer.Render(renderContext, surface, frameState); - }); - - m_xcuiInputSource.ClearFrameTransients(); + FrameLegacyImGuiHost(); } } // namespace NewEditor diff --git a/new_editor/src/Application.h b/new_editor/src/Application.h index 9dd130b9..b2f3417b 100644 --- a/new_editor/src/Application.h +++ b/new_editor/src/Application.h @@ -9,6 +9,8 @@ #include "Platform/D3D12WindowRenderer.h" #include "Rendering/MainWindowNativeBackdropRenderer.h" #include "XCUIBackend/IWindowUICompositor.h" +#include "XCUIBackend/NativeXCUIPanelCanvasHost.h" +#include "XCUIBackend/XCUIDemoRuntime.h" #include "XCUIBackend/XCUIHostedPreviewPresenter.h" #include "XCUIBackend/XCUIInputBridge.h" #include "XCUIBackend/XCUILayoutLabRuntime.h" @@ -36,6 +38,10 @@ public: using ShellHostedPreviewMode = ::XCEngine::Editor::XCUIBackend::XCUIShellHostedPreviewMode; using ShellPanelChromeState = ::XCEngine::Editor::XCUIBackend::XCUIShellPanelChromeState; using ShellViewToggleState = ::XCEngine::Editor::XCUIBackend::XCUIShellViewToggleState; + enum class WindowHostMode : std::uint8_t { + NativeXCUI = 0, + LegacyImGui + }; struct ShellCommandIds { static constexpr const char* ToggleXCUIDemoPanel = @@ -297,6 +303,7 @@ private: bool CreateMainWindow(HINSTANCE instance, int nCmdShow); bool InitializeRenderer(); void InitializeWindowCompositor(); + void InitializeNativeShell(); void ShutdownWindowCompositor(); void ShutdownRenderer(); void DestroyHostedPreviewSurfaces(); @@ -329,7 +336,14 @@ private: const ::XCEngine::Rendering::RenderContext& renderContext, const ::XCEngine::UI::UIDrawData& drawData); void ConfigureShellCommandRouter(); - void DispatchShellShortcuts(); + ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta DispatchShellShortcuts( + const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot& snapshot); + bool IsNativeWindowHostEnabled() const; + ::XCEngine::UI::UIDrawData BuildNativeShellDrawData( + const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameSnapshot& shellSnapshot, + const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta& shellFrameDelta); + void FrameLegacyImGuiHost(); + void FrameNativeXCUIHost(); void RenderShellChrome(); void RenderHostedPreviewHud(); void RenderQueuedHostedPreviews( @@ -351,6 +365,16 @@ private: ::XCEngine::Editor::XCUIBackend::XCUIRHIRenderBackend m_hostedPreviewRenderBackend; ShellChromeState m_shellChromeState = {}; std::vector m_hostedPreviewSurfaces = {}; + WindowHostMode m_windowHostMode = WindowHostMode::NativeXCUI; + ::XCEngine::Editor::XCUIBackend::XCUIDemoRuntime m_nativeDemoRuntime; + ::XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime m_nativeLayoutRuntime; + ::XCEngine::Editor::XCUIBackend::XCUIInputBridge m_nativeDemoInputBridge; + ::XCEngine::Editor::XCUIBackend::XCUIInputBridge m_nativeLayoutInputBridge; + ::XCEngine::Editor::XCUIBackend::NativeXCUIPanelCanvasHost m_nativeDemoCanvasHost; + ::XCEngine::Editor::XCUIBackend::NativeXCUIPanelCanvasHost m_nativeLayoutCanvasHost; + ShellPanelId m_nativeActivePanel = ShellPanelId::XCUIDemo; + bool m_nativeDemoReloadSucceeded = false; + bool m_nativeLayoutReloadSucceeded = false; ::XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime m_nativeOverlayRuntime; MainWindowNativeBackdropRenderer m_nativeBackdropRenderer; bool m_running = false; diff --git a/new_editor/src/XCUIBackend/NativeWindowUICompositor.cpp b/new_editor/src/XCUIBackend/NativeWindowUICompositor.cpp new file mode 100644 index 00000000..ba6fbf52 --- /dev/null +++ b/new_editor/src/XCUIBackend/NativeWindowUICompositor.cpp @@ -0,0 +1,218 @@ +#include "XCUIBackend/NativeWindowUICompositor.h" + +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace { + +bool PrepareSwapChainRender( + ::XCEngine::Editor::Platform::D3D12WindowRenderer& windowRenderer, + const float clearColor[4], + ::XCEngine::Rendering::RenderContext& outRenderContext, + const ::XCEngine::Rendering::RenderSurface*& outRenderSurface, + ::XCEngine::RHI::RHIResourceView*& outRenderTargetView) { + outRenderContext = windowRenderer.GetRenderContext(); + outRenderSurface = windowRenderer.GetCurrentRenderSurface(); + outRenderTargetView = nullptr; + + if (!outRenderContext.IsValid() || + outRenderSurface == nullptr || + outRenderContext.commandList == nullptr || + windowRenderer.GetSwapChain() == nullptr) { + return false; + } + + const auto& colorAttachments = outRenderSurface->GetColorAttachments(); + if (colorAttachments.empty() || colorAttachments[0] == nullptr) { + return false; + } + + outRenderTargetView = colorAttachments[0]; + outRenderContext.commandList->TransitionBarrier( + outRenderTargetView, + ::XCEngine::RHI::ResourceStates::Present, + ::XCEngine::RHI::ResourceStates::RenderTarget); + outRenderContext.commandList->SetRenderTargets(1, &outRenderTargetView, nullptr); + outRenderContext.commandList->ClearRenderTarget(outRenderTargetView, clearColor); + return true; +} + +void RebindSwapChainRenderTarget( + ::XCEngine::Rendering::RenderContext& renderContext, + ::XCEngine::RHI::RHIResourceView* renderTargetView) { + if (renderContext.commandList == nullptr || renderTargetView == nullptr) { + return; + } + + renderContext.commandList->SetRenderTargets(1, &renderTargetView, nullptr); +} + +void PresentSwapChainRender( + ::XCEngine::Editor::Platform::D3D12WindowRenderer& windowRenderer, + ::XCEngine::Rendering::RenderContext& renderContext, + ::XCEngine::RHI::RHIResourceView* renderTargetView) { + if (renderContext.commandList == nullptr || + renderContext.commandQueue == nullptr || + renderTargetView == nullptr || + windowRenderer.GetSwapChain() == nullptr) { + return; + } + + renderContext.commandList->TransitionBarrier( + renderTargetView, + ::XCEngine::RHI::ResourceStates::RenderTarget, + ::XCEngine::RHI::ResourceStates::Present); + renderContext.commandList->Close(); + + void* commandLists[] = { renderContext.commandList }; + renderContext.commandQueue->ExecuteCommandLists(1, commandLists); + windowRenderer.GetSwapChain()->Present(1, 0); +} + +} // namespace + +bool NativeWindowUICompositor::Initialize( + HWND hwnd, + ::XCEngine::Editor::Platform::D3D12WindowRenderer& windowRenderer, + const ConfigureFontsCallback& configureFonts) { + (void)configureFonts; + + m_hwnd = hwnd; + m_windowRenderer = hwnd != nullptr ? &windowRenderer : nullptr; + m_renderBackend.Shutdown(); + m_renderBackend.ResetStats(); + m_pendingRenderPacket.Clear(); + m_lastPresentStats = {}; + return m_windowRenderer != nullptr; +} + +void NativeWindowUICompositor::Shutdown() { + m_renderBackend.Shutdown(); + m_pendingRenderPacket.Clear(); + m_lastPresentStats = {}; + m_windowRenderer = nullptr; + m_hwnd = nullptr; +} + +bool NativeWindowUICompositor::HandleWindowMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + (void)hwnd; + (void)message; + (void)wParam; + (void)lParam; + return false; +} + +void NativeWindowUICompositor::RenderFrame( + const float clearColor[4], + const UiRenderCallback& renderUi, + const RenderCallback& beforeUiRender, + const RenderCallback& afterUiRender) { + (void)renderUi; + + m_lastPresentStats = {}; + if (m_windowRenderer == nullptr) { + return; + } + + ::XCEngine::Rendering::RenderContext renderContext = {}; + const ::XCEngine::Rendering::RenderSurface* renderSurface = nullptr; + ::XCEngine::RHI::RHIResourceView* renderTargetView = nullptr; + if (!PrepareSwapChainRender( + *m_windowRenderer, + clearColor, + renderContext, + renderSurface, + renderTargetView)) { + return; + } + + if (beforeUiRender) { + beforeUiRender(renderContext, *renderSurface); + RebindSwapChainRenderTarget(renderContext, renderTargetView); + } + + m_lastPresentStats.hadPendingPacket = m_pendingRenderPacket.HasDrawData(); + m_lastPresentStats.submittedDrawListCount = m_pendingRenderPacket.drawData.GetDrawListCount(); + m_lastPresentStats.submittedCommandCount = m_pendingRenderPacket.drawData.GetTotalCommandCount(); + + if (m_pendingRenderPacket.textAtlasProvider != nullptr) { + m_renderBackend.SetTextAtlasProvider(m_pendingRenderPacket.textAtlasProvider); + } else { + m_renderBackend.SetTextAtlasProvider(nullptr); + } + + if (m_lastPresentStats.hadPendingPacket) { + m_lastPresentStats.renderedNativeOverlay = + m_renderBackend.Render(renderContext, *renderSurface, m_pendingRenderPacket.drawData); + m_lastPresentStats.overlayStats = m_renderBackend.GetLastOverlayStats(); + } else { + m_renderBackend.ResetStats(); + m_lastPresentStats.overlayStats = m_renderBackend.GetLastOverlayStats(); + } + + m_pendingRenderPacket.Clear(); + + if (afterUiRender) { + RebindSwapChainRenderTarget(renderContext, renderTargetView); + afterUiRender(renderContext, *renderSurface); + } + + PresentSwapChainRender(*m_windowRenderer, renderContext, renderTargetView); +} + +bool NativeWindowUICompositor::CreateTextureDescriptor( + ::XCEngine::RHI::RHIDevice* device, + ::XCEngine::RHI::RHITexture* texture, + UITextureRegistration& outRegistration) { + (void)device; + (void)texture; + outRegistration = {}; + return false; +} + +void NativeWindowUICompositor::FreeTextureDescriptor(const UITextureRegistration& registration) { + (void)registration; +} + +void NativeWindowUICompositor::SubmitRenderPacket(const XCUINativeWindowRenderPacket& packet) { + m_pendingRenderPacket = packet; +} + +void NativeWindowUICompositor::SubmitRenderPacket( + const ::XCEngine::UI::UIDrawData& drawData, + const IXCUITextAtlasProvider* textAtlasProvider) { + m_pendingRenderPacket.drawData = drawData; + m_pendingRenderPacket.textAtlasProvider = textAtlasProvider; +} + +void NativeWindowUICompositor::ClearPendingRenderPacket() { + m_pendingRenderPacket.Clear(); +} + +bool NativeWindowUICompositor::HasPendingRenderPacket() const { + return m_pendingRenderPacket.HasDrawData(); +} + +const XCUINativeWindowRenderPacket& NativeWindowUICompositor::GetPendingRenderPacket() const { + return m_pendingRenderPacket; +} + +const XCUINativeWindowPresentStats& NativeWindowUICompositor::GetLastPresentStats() const { + return m_lastPresentStats; +} + +std::unique_ptr CreateNativeWindowUICompositor() { + return std::make_unique(); +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/NativeWindowUICompositor.h b/new_editor/src/XCUIBackend/NativeWindowUICompositor.h new file mode 100644 index 00000000..f7ac4771 --- /dev/null +++ b/new_editor/src/XCUIBackend/NativeWindowUICompositor.h @@ -0,0 +1,76 @@ +#pragma once + +#include "XCUIBackend/IWindowUICompositor.h" +#include "XCUIBackend/IXCUITextAtlasProvider.h" +#include "XCUIBackend/XCUIRHIRenderBackend.h" + +#include + +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +struct XCUINativeWindowRenderPacket { + ::XCEngine::UI::UIDrawData drawData = {}; + const IXCUITextAtlasProvider* textAtlasProvider = nullptr; + + void Clear() { + drawData.Clear(); + textAtlasProvider = nullptr; + } + + bool HasDrawData() const { + return drawData.GetDrawListCount() > 0u && drawData.GetTotalCommandCount() > 0u; + } +}; + +struct XCUINativeWindowPresentStats { + bool hadPendingPacket = false; + bool renderedNativeOverlay = false; + std::size_t submittedDrawListCount = 0; + std::size_t submittedCommandCount = 0; + XCUIRHIRenderBackend::OverlayStats overlayStats = {}; +}; + +class NativeWindowUICompositor final : public IWindowUICompositor { +public: + bool Initialize( + HWND hwnd, + ::XCEngine::Editor::Platform::D3D12WindowRenderer& windowRenderer, + const ConfigureFontsCallback& configureFonts) override; + void Shutdown() override; + bool HandleWindowMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) override; + void RenderFrame( + const float clearColor[4], + const UiRenderCallback& renderUi, + const RenderCallback& beforeUiRender = {}, + const RenderCallback& afterUiRender = {}) override; + bool CreateTextureDescriptor( + ::XCEngine::RHI::RHIDevice* device, + ::XCEngine::RHI::RHITexture* texture, + UITextureRegistration& outRegistration) override; + void FreeTextureDescriptor(const UITextureRegistration& registration) override; + + void SubmitRenderPacket(const XCUINativeWindowRenderPacket& packet); + void SubmitRenderPacket( + const ::XCEngine::UI::UIDrawData& drawData, + const IXCUITextAtlasProvider* textAtlasProvider = nullptr); + void ClearPendingRenderPacket(); + bool HasPendingRenderPacket() const; + const XCUINativeWindowRenderPacket& GetPendingRenderPacket() const; + const XCUINativeWindowPresentStats& GetLastPresentStats() const; + +private: + HWND m_hwnd = nullptr; + ::XCEngine::Editor::Platform::D3D12WindowRenderer* m_windowRenderer = nullptr; + XCUIRHIRenderBackend m_renderBackend = {}; + XCUINativeWindowRenderPacket m_pendingRenderPacket = {}; + XCUINativeWindowPresentStats m_lastPresentStats = {}; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/NativeXCUIPanelCanvasHost.h b/new_editor/src/XCUIBackend/NativeXCUIPanelCanvasHost.h new file mode 100644 index 00000000..4d3146ec --- /dev/null +++ b/new_editor/src/XCUIBackend/NativeXCUIPanelCanvasHost.h @@ -0,0 +1,294 @@ +#pragma once + +#include "XCUIBackend/XCUIPanelCanvasHost.h" + +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace XCUIBackend { + +namespace detail { + +inline std::string CopyCanvasLabel(const char* text) { + return text != nullptr ? std::string(text) : std::string(); +} + +inline ::XCEngine::UI::UIColor NativeCanvasPlaceholderFillColor() { + return ::XCEngine::UI::UIColor(18.0f / 255.0f, 24.0f / 255.0f, 32.0f / 255.0f, 1.0f); +} + +inline ::XCEngine::UI::UIColor NativeCanvasPlaceholderStrokeColor() { + return ::XCEngine::UI::UIColor(54.0f / 255.0f, 72.0f / 255.0f, 94.0f / 255.0f, 1.0f); +} + +inline ::XCEngine::UI::UIColor NativeCanvasPrimaryTextColor() { + return ::XCEngine::UI::UIColor(191.0f / 255.0f, 205.0f / 255.0f, 224.0f / 255.0f, 1.0f); +} + +inline ::XCEngine::UI::UIColor NativeCanvasSecondaryTextColor() { + return ::XCEngine::UI::UIColor(132.0f / 255.0f, 147.0f / 255.0f, 170.0f / 255.0f, 1.0f); +} + +inline XCUIPanelCanvasSession NormalizeNativeCanvasSession( + const XCUIPanelCanvasRequest& request, + const XCUIPanelCanvasSession* configuredSession) { + if (configuredSession == nullptr) { + return BuildPassiveXCUIPanelCanvasSession(request); + } + + XCUIPanelCanvasSession session = *configuredSession; + const float fallbackHostHeight = request.height > 0.0f ? request.height : 0.0f; + if (session.hostRect.height <= 0.0f && fallbackHostHeight > 0.0f) { + session.hostRect.height = fallbackHostHeight; + } + + if (session.canvasRect.width <= 0.0f || session.canvasRect.height <= 0.0f) { + const float topInset = request.topInset > 0.0f ? request.topInset : 0.0f; + const float clampedTopInset = + session.hostRect.height > 0.0f + ? (std::min)(topInset, session.hostRect.height) + : 0.0f; + session.canvasRect = ::XCEngine::UI::UIRect( + session.hostRect.x, + session.hostRect.y + clampedTopInset, + session.hostRect.width, + (std::max)(0.0f, session.hostRect.height - clampedTopInset)); + } + + session.validCanvas = + session.canvasRect.width > 1.0f && + session.canvasRect.height > 1.0f; + if (!session.validCanvas) { + session.hovered = false; + } + return session; +} + +inline void RecordPlaceholder( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& rect, + const std::string& title, + const std::string& subtitle) { + if (rect.width <= 1.0f || rect.height <= 1.0f) { + return; + } + + drawList.AddFilledRect(rect, NativeCanvasPlaceholderFillColor(), 8.0f); + drawList.AddRectOutline(rect, NativeCanvasPlaceholderStrokeColor(), 1.0f, 8.0f); + if (!title.empty()) { + drawList.AddText( + ::XCEngine::UI::UIPoint(rect.x + 14.0f, rect.y + 14.0f), + title, + NativeCanvasPrimaryTextColor()); + } + if (!subtitle.empty()) { + drawList.AddText( + ::XCEngine::UI::UIPoint(rect.x + 14.0f, rect.y + 36.0f), + subtitle, + NativeCanvasSecondaryTextColor()); + } +} + +inline void RecordBadge( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& canvasRect, + const std::string& title, + const std::string& subtitle) { + if (title.empty()) { + return; + } + + const ::XCEngine::UI::UIRect badgeRect( + canvasRect.x + 10.0f, + canvasRect.y + 10.0f, + 290.0f, + 42.0f); + drawList.AddFilledRect( + badgeRect, + ::XCEngine::UI::UIColor(16.0f / 255.0f, 22.0f / 255.0f, 30.0f / 255.0f, 216.0f / 255.0f), + 8.0f); + drawList.AddRectOutline(badgeRect, NativeCanvasPlaceholderStrokeColor(), 1.0f, 8.0f); + drawList.AddText( + ::XCEngine::UI::UIPoint(badgeRect.x + 10.0f, badgeRect.y + 8.0f), + title, + NativeCanvasPrimaryTextColor()); + if (!subtitle.empty()) { + drawList.AddText( + ::XCEngine::UI::UIPoint(badgeRect.x + 10.0f, badgeRect.y + 24.0f), + subtitle, + NativeCanvasSecondaryTextColor()); + } +} + +} // namespace detail + +class NativeXCUIPanelCanvasHost final : public IXCUIPanelCanvasHost { +public: + const char* GetDebugName() const override { + return "NativeXCUIPanelCanvasHost"; + } + + XCUIPanelCanvasHostBackend GetBackend() const override { + return XCUIPanelCanvasHostBackend::Native; + } + + XCUIPanelCanvasHostCapabilities GetCapabilities() const override { + XCUIPanelCanvasHostCapabilities capabilities = {}; + capabilities.supportsHostedSurfaceImages = true; + capabilities.supportsPrimitiveOverlays = true; + capabilities.supportsExternallyDrivenSession = true; + return capabilities; + } + + void SetCanvasSession(const XCUIPanelCanvasSession& session) { + m_configuredSession = session; + m_hasConfiguredSession = true; + } + + void ClearCanvasSession() { + m_configuredSession = {}; + m_hasConfiguredSession = false; + } + + bool HasConfiguredSession() const { + return m_hasConfiguredSession; + } + + const XCUIPanelCanvasSession& GetConfiguredSession() const { + return m_configuredSession; + } + + XCUIPanelCanvasSession BeginCanvas(const XCUIPanelCanvasRequest& request) override { + m_currentFrame = {}; + m_currentFrame.childId = ResolveXCUIPanelCanvasChildId(request, "NativeXCUIPanelCanvasHost"); + m_currentFrame.session = detail::NormalizeNativeCanvasSession( + request, + m_hasConfiguredSession ? &m_configuredSession : nullptr); + m_currentFrame.bordered = request.bordered; + m_currentFrame.drawPreviewFrame = request.drawPreviewFrame; + m_currentFrame.showingSurfaceImage = request.showSurfaceImage && request.surfaceImage.IsValid(); + m_currentFrame.placeholderTitle = detail::CopyCanvasLabel(request.placeholderTitle); + m_currentFrame.placeholderSubtitle = detail::CopyCanvasLabel(request.placeholderSubtitle); + m_currentFrame.badgeTitle = detail::CopyCanvasLabel(request.badgeTitle); + m_currentFrame.badgeSubtitle = detail::CopyCanvasLabel(request.badgeSubtitle); + m_currentFrame.surfaceImage = request.surfaceImage; + + m_overlayDrawList = nullptr; + m_clipDepth = 0u; + + if (m_currentFrame.session.validCanvas) { + const bool shouldRecordPlaceholder = + !m_currentFrame.showingSurfaceImage && + (!m_currentFrame.placeholderTitle.empty() || !m_currentFrame.placeholderSubtitle.empty()); + if (shouldRecordPlaceholder) { + ::XCEngine::UI::UIDrawList& drawList = EnsureOverlayDrawList(); + detail::RecordPlaceholder( + drawList, + m_currentFrame.session.canvasRect, + m_currentFrame.placeholderTitle, + m_currentFrame.placeholderSubtitle); + } + + if (request.drawPreviewFrame) { + DrawOutlineRect( + m_currentFrame.session.canvasRect, + detail::NativeCanvasPlaceholderStrokeColor(), + 1.0f, + 8.0f); + } + + if (!m_currentFrame.badgeTitle.empty()) { + ::XCEngine::UI::UIDrawList& drawList = EnsureOverlayDrawList(); + detail::RecordBadge( + drawList, + m_currentFrame.session.canvasRect, + m_currentFrame.badgeTitle, + m_currentFrame.badgeSubtitle); + } + } + + return m_currentFrame.session; + } + + void DrawFilledRect( + const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIColor& color, + float rounding = 0.0f) override { + if (rect.width <= 0.0f || rect.height <= 0.0f) { + return; + } + + EnsureOverlayDrawList().AddFilledRect(rect, color, rounding); + } + + void DrawOutlineRect( + const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIColor& color, + float thickness = 1.0f, + float rounding = 0.0f) override { + if (rect.width <= 0.0f || rect.height <= 0.0f) { + return; + } + + EnsureOverlayDrawList().AddRectOutline(rect, color, thickness, rounding); + } + + void DrawText( + const ::XCEngine::UI::UIPoint& position, + std::string_view text, + const ::XCEngine::UI::UIColor& color, + float fontSize = 0.0f) override { + if (text.empty()) { + return; + } + + EnsureOverlayDrawList().AddText(position, std::string(text), color, fontSize); + } + + void EndCanvas() override { + if (m_overlayDrawList != nullptr) { + while (m_clipDepth > 0u) { + m_overlayDrawList->PopClipRect(); + --m_clipDepth; + } + } + + m_overlayDrawList = nullptr; + } + + bool TryGetLatestFrameSnapshot(XCUIPanelCanvasFrameSnapshot& outSnapshot) const override { + outSnapshot = m_currentFrame; + return !outSnapshot.childId.empty(); + } + +private: + ::XCEngine::UI::UIDrawList& EnsureOverlayDrawList() { + if (m_overlayDrawList == nullptr) { + ::XCEngine::UI::UIDrawList& drawList = + m_currentFrame.overlayDrawData.EmplaceDrawList(m_currentFrame.childId + ".overlay"); + drawList.PushClipRect(m_currentFrame.session.canvasRect, true); + m_overlayDrawList = &drawList; + m_clipDepth = 1u; + } + + return *m_overlayDrawList; + } + + bool m_hasConfiguredSession = false; + XCUIPanelCanvasSession m_configuredSession = {}; + XCUIPanelCanvasFrameSnapshot m_currentFrame = {}; + ::XCEngine::UI::UIDrawList* m_overlayDrawList = nullptr; + std::size_t m_clipDepth = 0u; +}; + +inline std::unique_ptr CreateNativeXCUIPanelCanvasHost() { + return std::make_unique(); +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIInputBridge.cpp b/new_editor/src/XCUIBackend/XCUIInputBridge.cpp index 9168b78e..649ed56a 100644 --- a/new_editor/src/XCUIBackend/XCUIInputBridge.cpp +++ b/new_editor/src/XCUIBackend/XCUIInputBridge.cpp @@ -2,8 +2,6 @@ #include #include -#include -#include namespace XCEngine { namespace Editor { @@ -64,13 +62,6 @@ void AppendUniqueKeyCode(std::vector& keyCodes, std::int32_t keyCo } } -bool IsPointerPositionValid(const ImVec2& position) { - return std::isfinite(position.x) && - std::isfinite(position.y) && - position.x > -std::numeric_limits::max() * 0.5f && - position.y > -std::numeric_limits::max() * 0.5f; -} - std::uint64_t GetTimestampNanoseconds() { return static_cast( std::chrono::duration_cast( diff --git a/new_editor/src/XCUIBackend/XCUIInputBridge.h b/new_editor/src/XCUIBackend/XCUIInputBridge.h index 6ad237b0..21134024 100644 --- a/new_editor/src/XCUIBackend/XCUIInputBridge.h +++ b/new_editor/src/XCUIBackend/XCUIInputBridge.h @@ -3,8 +3,6 @@ #include #include -#include - #include #include #include diff --git a/tests/NewEditor/CMakeLists.txt b/tests/NewEditor/CMakeLists.txt index ea94bc19..34dab03c 100644 --- a/tests/NewEditor/CMakeLists.txt +++ b/tests/NewEditor/CMakeLists.txt @@ -136,6 +136,12 @@ set(NEW_EDITOR_WINDOW_UI_COMPOSITOR_HEADER set(NEW_EDITOR_IMGUI_WINDOW_UI_COMPOSITOR_HEADER ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiWindowUICompositor.h ) +set(NEW_EDITOR_NATIVE_WINDOW_UI_COMPOSITOR_HEADER + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/NativeWindowUICompositor.h +) +set(NEW_EDITOR_NATIVE_WINDOW_UI_COMPOSITOR_SOURCE + ${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/NativeWindowUICompositor.cpp +) set(NEW_EDITOR_NATIVE_BACKDROP_RENDERER_HEADER ${CMAKE_SOURCE_DIR}/new_editor/src/Rendering/MainWindowNativeBackdropRenderer.h ) @@ -436,6 +442,43 @@ else() message(STATUS "Skipping new_editor_imgui_window_ui_compositor_tests because compositor headers or the test source are missing.") endif() +if(EXISTS "${NEW_EDITOR_WINDOW_UI_COMPOSITOR_HEADER}" AND + EXISTS "${NEW_EDITOR_NATIVE_WINDOW_UI_COMPOSITOR_HEADER}" AND + EXISTS "${NEW_EDITOR_NATIVE_WINDOW_UI_COMPOSITOR_SOURCE}" AND + EXISTS "${NEW_EDITOR_RHI_RENDER_BACKEND_SOURCE}" AND + EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE}" AND + EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_native_window_ui_compositor.cpp") + add_executable(new_editor_native_window_ui_compositor_tests + test_native_window_ui_compositor.cpp + ${NEW_EDITOR_NATIVE_WINDOW_UI_COMPOSITOR_SOURCE} + ${NEW_EDITOR_RHI_RENDER_BACKEND_SOURCE} + ${NEW_EDITOR_RHI_COMMAND_COMPILER_SOURCE} + ) + + xcengine_configure_new_editor_test_target(new_editor_native_window_ui_compositor_tests) + + target_link_libraries(new_editor_native_window_ui_compositor_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + user32 + comdlg32 + ) + + target_include_directories(new_editor_native_window_ui_compositor_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ${CMAKE_SOURCE_DIR}/editor/src + ${CMAKE_BINARY_DIR}/_deps/imgui-src + ${CMAKE_BINARY_DIR}/_deps/imgui-src/backends + ) + + xcengine_discover_new_editor_gtests(new_editor_native_window_ui_compositor_tests) +else() + message(STATUS "Skipping new_editor_native_window_ui_compositor_tests because native compositor/backend files or the test source are missing.") +endif() + if(EXISTS "${NEW_EDITOR_NATIVE_BACKDROP_RENDERER_HEADER}") add_executable(new_editor_native_backdrop_renderer_api_tests test_main_window_native_backdrop_renderer_api.cpp @@ -824,3 +867,28 @@ if(EXISTS "${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiXCUIPanelCanvasHo else() message(STATUS "Skipping new_editor_imgui_xcui_panel_canvas_host_tests because the ImGui host header, test source, or ImGui sources are missing.") endif() + +if(EXISTS "${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/NativeXCUIPanelCanvasHost.h" AND + EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_native_xcui_panel_canvas_host.cpp") + add_executable(new_editor_native_xcui_panel_canvas_host_tests + test_native_xcui_panel_canvas_host.cpp + ) + + xcengine_configure_new_editor_test_target(new_editor_native_xcui_panel_canvas_host_tests) + + target_link_libraries(new_editor_native_xcui_panel_canvas_host_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main + ) + + target_include_directories(new_editor_native_xcui_panel_canvas_host_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/src + ) + + xcengine_discover_new_editor_gtests(new_editor_native_xcui_panel_canvas_host_tests) +else() + message(STATUS "Skipping new_editor_native_xcui_panel_canvas_host_tests because the native host header or test source is missing.") +endif() diff --git a/tests/NewEditor/test_native_window_ui_compositor.cpp b/tests/NewEditor/test_native_window_ui_compositor.cpp new file mode 100644 index 00000000..d45a9965 --- /dev/null +++ b/tests/NewEditor/test_native_window_ui_compositor.cpp @@ -0,0 +1,212 @@ +#include + +#include "XCUIBackend/IWindowUICompositor.h" +#include "XCUIBackend/NativeWindowUICompositor.h" +#include "XCUIBackend/UITextureRegistration.h" + +#include + +#include +#include +#include + +namespace { + +using XCEngine::Editor::Platform::D3D12WindowRenderer; +using XCEngine::Editor::XCUIBackend::CreateNativeWindowUICompositor; +using XCEngine::Editor::XCUIBackend::IXCUITextAtlasProvider; +using XCEngine::Editor::XCUIBackend::IWindowUICompositor; +using XCEngine::Editor::XCUIBackend::NativeWindowUICompositor; +using XCEngine::Editor::XCUIBackend::UITextureRegistration; +using XCEngine::Editor::XCUIBackend::XCUINativeWindowPresentStats; +using XCEngine::Editor::XCUIBackend::XCUINativeWindowRenderPacket; + +HWND MakeFakeHwnd() { + return reinterpret_cast(static_cast(0x2345u)); +} + +class StubTextAtlasProvider final : public IXCUITextAtlasProvider { +public: + bool GetAtlasTextureView(PixelFormat preferredFormat, AtlasTextureView& outView) const override { + (void)preferredFormat; + outView = {}; + return false; + } + + std::size_t GetFontCount() const override { + return 0u; + } + + FontHandle GetFont(std::size_t index) const override { + (void)index; + return {}; + } + + FontHandle GetDefaultFont() const override { + return {}; + } + + bool GetFontInfo(FontHandle font, FontInfo& outInfo) const override { + (void)font; + outInfo = {}; + return false; + } + + bool GetBakedFontInfo(FontHandle font, float fontSize, BakedFontInfo& outInfo) const override { + (void)font; + (void)fontSize; + outInfo = {}; + return false; + } + + bool FindGlyph(FontHandle font, float fontSize, std::uint32_t codepoint, GlyphInfo& outInfo) const override { + (void)font; + (void)fontSize; + (void)codepoint; + outInfo = {}; + return false; + } +}; + +XCEngine::UI::UIDrawData MakeDrawData() { + XCEngine::UI::UIDrawData drawData = {}; + drawData.EmplaceDrawList("NativeOverlay").AddFilledRect( + XCEngine::UI::UIRect(10.0f, 12.0f, 48.0f, 24.0f), + XCEngine::UI::UIColor(0.2f, 0.4f, 0.8f, 1.0f)); + return drawData; +} + +TEST(NativeWindowUICompositorTest, RenderPacketReportsDrawDataPresenceAndClearResetsPayload) { + XCUINativeWindowRenderPacket packet = {}; + EXPECT_FALSE(packet.HasDrawData()); + EXPECT_EQ(packet.textAtlasProvider, nullptr); + + StubTextAtlasProvider atlasProvider = {}; + packet.drawData = MakeDrawData(); + packet.textAtlasProvider = &atlasProvider; + + EXPECT_TRUE(packet.HasDrawData()); + EXPECT_EQ(packet.drawData.GetDrawListCount(), 1u); + EXPECT_EQ(packet.drawData.GetTotalCommandCount(), 1u); + EXPECT_EQ(packet.textAtlasProvider, &atlasProvider); + + packet.Clear(); + EXPECT_FALSE(packet.HasDrawData()); + EXPECT_EQ(packet.drawData.GetDrawListCount(), 0u); + EXPECT_EQ(packet.drawData.GetTotalCommandCount(), 0u); + EXPECT_EQ(packet.textAtlasProvider, nullptr); +} + +TEST(NativeWindowUICompositorTest, SubmitAndClearPendingPacketTracksCopiedDrawDataAndAtlasProvider) { + NativeWindowUICompositor compositor = {}; + StubTextAtlasProvider atlasProvider = {}; + const XCEngine::UI::UIDrawData drawData = MakeDrawData(); + + compositor.SubmitRenderPacket(drawData, &atlasProvider); + ASSERT_TRUE(compositor.HasPendingRenderPacket()); + + const XCUINativeWindowRenderPacket& packet = compositor.GetPendingRenderPacket(); + EXPECT_TRUE(packet.HasDrawData()); + EXPECT_EQ(packet.drawData.GetDrawListCount(), 1u); + EXPECT_EQ(packet.drawData.GetTotalCommandCount(), 1u); + EXPECT_EQ(packet.textAtlasProvider, &atlasProvider); + + compositor.ClearPendingRenderPacket(); + EXPECT_FALSE(compositor.HasPendingRenderPacket()); + EXPECT_FALSE(compositor.GetPendingRenderPacket().HasDrawData()); + EXPECT_EQ(compositor.GetPendingRenderPacket().textAtlasProvider, nullptr); +} + +TEST(NativeWindowUICompositorTest, InitializeAndShutdownResetStateAlongSafePaths) { + NativeWindowUICompositor compositor = {}; + D3D12WindowRenderer renderer = {}; + compositor.SubmitRenderPacket(MakeDrawData(), nullptr); + ASSERT_TRUE(compositor.HasPendingRenderPacket()); + + bool configureFontsCalled = false; + EXPECT_FALSE(compositor.Initialize( + nullptr, + renderer, + [&configureFontsCalled]() { configureFontsCalled = true; })); + EXPECT_FALSE(configureFontsCalled); + EXPECT_FALSE(compositor.HasPendingRenderPacket()); + + compositor.SubmitRenderPacket(MakeDrawData(), nullptr); + EXPECT_TRUE(compositor.Initialize( + MakeFakeHwnd(), + renderer, + [&configureFontsCalled]() { configureFontsCalled = true; })); + EXPECT_FALSE(configureFontsCalled); + EXPECT_FALSE(compositor.HasPendingRenderPacket()); + + compositor.Shutdown(); + EXPECT_FALSE(compositor.HasPendingRenderPacket()); + const XCUINativeWindowPresentStats& stats = compositor.GetLastPresentStats(); + EXPECT_FALSE(stats.hadPendingPacket); + EXPECT_FALSE(stats.renderedNativeOverlay); + EXPECT_EQ(stats.submittedDrawListCount, 0u); + EXPECT_EQ(stats.submittedCommandCount, 0u); +} + +TEST(NativeWindowUICompositorTest, RenderFrameWithUnpreparedRendererSkipsCallbacksAndKeepsPendingPacket) { + NativeWindowUICompositor compositor = {}; + D3D12WindowRenderer renderer = {}; + ASSERT_TRUE(compositor.Initialize(MakeFakeHwnd(), renderer, {})); + + compositor.SubmitRenderPacket(MakeDrawData(), nullptr); + ASSERT_TRUE(compositor.HasPendingRenderPacket()); + + bool uiRendered = false; + bool beforeUiRendered = false; + bool afterUiRendered = false; + compositor.RenderFrame( + std::array{ 0.1f, 0.2f, 0.3f, 1.0f }.data(), + [&uiRendered]() { uiRendered = true; }, + [&beforeUiRendered](const ::XCEngine::Rendering::RenderContext&, const ::XCEngine::Rendering::RenderSurface&) { + beforeUiRendered = true; + }, + [&afterUiRendered](const ::XCEngine::Rendering::RenderContext&, const ::XCEngine::Rendering::RenderSurface&) { + afterUiRendered = true; + }); + + EXPECT_FALSE(uiRendered); + EXPECT_FALSE(beforeUiRendered); + EXPECT_FALSE(afterUiRendered); + EXPECT_TRUE(compositor.HasPendingRenderPacket()); + + const XCUINativeWindowPresentStats& stats = compositor.GetLastPresentStats(); + EXPECT_FALSE(stats.hadPendingPacket); + EXPECT_FALSE(stats.renderedNativeOverlay); + EXPECT_EQ(stats.submittedDrawListCount, 0u); + EXPECT_EQ(stats.submittedCommandCount, 0u); +} + +TEST(NativeWindowUICompositorTest, InterfaceFactoryReturnsSafeNativeCompositorDefaults) { + std::unique_ptr compositor = CreateNativeWindowUICompositor(); + ASSERT_NE(compositor, nullptr); + + D3D12WindowRenderer renderer = {}; + bool configureFontsCalled = false; + EXPECT_FALSE(compositor->Initialize( + nullptr, + renderer, + [&configureFontsCalled]() { configureFontsCalled = true; })); + EXPECT_FALSE(configureFontsCalled); + EXPECT_FALSE(compositor->HandleWindowMessage(MakeFakeHwnd(), WM_CLOSE, 0u, 0u)); + + UITextureRegistration registration = {}; + EXPECT_FALSE(compositor->CreateTextureDescriptor(nullptr, nullptr, registration)); + EXPECT_EQ(registration.texture.nativeHandle, 0u); + + bool uiRendered = false; + compositor->RenderFrame( + std::array{ 0.0f, 0.0f, 0.0f, 1.0f }.data(), + [&uiRendered]() { uiRendered = true; }, + {}, + {}); + EXPECT_FALSE(uiRendered); + + compositor->Shutdown(); +} + +} // namespace diff --git a/tests/NewEditor/test_native_xcui_panel_canvas_host.cpp b/tests/NewEditor/test_native_xcui_panel_canvas_host.cpp new file mode 100644 index 00000000..7789ebf7 --- /dev/null +++ b/tests/NewEditor/test_native_xcui_panel_canvas_host.cpp @@ -0,0 +1,176 @@ +#include + +#include "XCUIBackend/NativeXCUIPanelCanvasHost.h" + +namespace { + +using XCEngine::Editor::XCUIBackend::CreateNativeXCUIPanelCanvasHost; +using XCEngine::Editor::XCUIBackend::IXCUIPanelCanvasHost; +using XCEngine::Editor::XCUIBackend::NativeXCUIPanelCanvasHost; +using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasFrameSnapshot; +using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasHostBackend; +using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasHostCapabilities; +using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasRequest; +using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession; + +XCEngine::UI::UITextureHandle MakeSurfaceTextureHandle(std::uintptr_t nativeHandle, std::uint32_t width, std::uint32_t height) { + XCEngine::UI::UITextureHandle texture = {}; + texture.nativeHandle = nativeHandle; + texture.width = width; + texture.height = height; + texture.kind = XCEngine::UI::UITextureHandleKind::ShaderResourceView; + return texture; +} + +TEST(NativeXCUIPanelCanvasHostTest, FactoryReportsNativeBackendCapabilitiesAndNoSnapshotBeforeBegin) { + std::unique_ptr host = CreateNativeXCUIPanelCanvasHost(); + ASSERT_NE(host, nullptr); + + EXPECT_STREQ(host->GetDebugName(), "NativeXCUIPanelCanvasHost"); + EXPECT_EQ(host->GetBackend(), XCUIPanelCanvasHostBackend::Native); + + const XCUIPanelCanvasHostCapabilities capabilities = host->GetCapabilities(); + EXPECT_FALSE(capabilities.supportsPointerHitTesting); + EXPECT_TRUE(capabilities.supportsHostedSurfaceImages); + EXPECT_TRUE(capabilities.supportsPrimitiveOverlays); + EXPECT_TRUE(capabilities.supportsExternallyDrivenSession); + + XCUIPanelCanvasFrameSnapshot snapshot = {}; + EXPECT_FALSE(host->TryGetLatestFrameSnapshot(snapshot)); + EXPECT_TRUE(snapshot.childId.empty()); + EXPECT_EQ(snapshot.overlayDrawData.GetDrawListCount(), 0u); +} + +TEST(NativeXCUIPanelCanvasHostTest, BeginCanvasWithConfiguredSessionCapturesSnapshotAndOverlayCommands) { + NativeXCUIPanelCanvasHost host = {}; + XCUIPanelCanvasSession configuredSession = {}; + configuredSession.hostRect = XCEngine::UI::UIRect(20.0f, 30.0f, 640.0f, 360.0f); + configuredSession.canvasRect = XCEngine::UI::UIRect(20.0f, 72.0f, 640.0f, 318.0f); + configuredSession.pointerPosition = XCEngine::UI::UIPoint(128.0f, 144.0f); + configuredSession.validCanvas = true; + configuredSession.hovered = true; + configuredSession.windowFocused = true; + host.SetCanvasSession(configuredSession); + + XCUIPanelCanvasRequest request = {}; + request.childId = "NativeDemoCanvas"; + request.height = 360.0f; + request.topInset = 42.0f; + request.placeholderTitle = "Placeholder"; + request.placeholderSubtitle = "Native host placeholder"; + request.badgeTitle = "XCUI Demo"; + request.badgeSubtitle = "native path"; + + const XCUIPanelCanvasSession session = host.BeginCanvas(request); + EXPECT_FLOAT_EQ(session.hostRect.x, configuredSession.hostRect.x); + EXPECT_FLOAT_EQ(session.hostRect.y, configuredSession.hostRect.y); + EXPECT_FLOAT_EQ(session.canvasRect.x, configuredSession.canvasRect.x); + EXPECT_FLOAT_EQ(session.canvasRect.y, configuredSession.canvasRect.y); + EXPECT_TRUE(session.validCanvas); + EXPECT_TRUE(session.hovered); + EXPECT_TRUE(session.windowFocused); + + host.DrawFilledRect( + XCEngine::UI::UIRect(40.0f, 90.0f, 120.0f, 60.0f), + XCEngine::UI::UIColor(0.8f, 0.2f, 0.1f, 1.0f), + 6.0f); + host.DrawOutlineRect( + XCEngine::UI::UIRect(42.0f, 92.0f, 118.0f, 58.0f), + XCEngine::UI::UIColor(0.2f, 0.8f, 0.3f, 1.0f), + 2.0f, + 4.0f); + host.DrawText( + XCEngine::UI::UIPoint(48.0f, 104.0f), + "hello native", + XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f), + 16.0f); + host.EndCanvas(); + + XCUIPanelCanvasFrameSnapshot snapshot = {}; + ASSERT_TRUE(host.TryGetLatestFrameSnapshot(snapshot)); + EXPECT_EQ(snapshot.childId, "NativeDemoCanvas"); + EXPECT_FALSE(snapshot.showingSurfaceImage); + EXPECT_TRUE(snapshot.drawPreviewFrame); + EXPECT_EQ(snapshot.placeholderTitle, "Placeholder"); + EXPECT_EQ(snapshot.placeholderSubtitle, "Native host placeholder"); + EXPECT_EQ(snapshot.badgeTitle, "XCUI Demo"); + EXPECT_EQ(snapshot.badgeSubtitle, "native path"); + EXPECT_TRUE(snapshot.session.validCanvas); + EXPECT_EQ(snapshot.overlayDrawData.GetDrawListCount(), 1u); + EXPECT_EQ(snapshot.overlayDrawData.GetDrawLists().front().GetDebugName(), "NativeDemoCanvas.overlay"); + EXPECT_EQ(snapshot.overlayDrawData.GetTotalCommandCount(), 14u); + EXPECT_EQ( + snapshot.overlayDrawData.GetDrawLists().front().GetCommands().front().type, + XCEngine::UI::UIDrawCommandType::PushClipRect); + EXPECT_EQ( + snapshot.overlayDrawData.GetDrawLists().front().GetCommands().back().type, + XCEngine::UI::UIDrawCommandType::PopClipRect); +} + +TEST(NativeXCUIPanelCanvasHostTest, SurfaceImagePathCapturesSurfaceAndPreviewFrameWithoutPlaceholder) { + NativeXCUIPanelCanvasHost host = {}; + XCUIPanelCanvasSession configuredSession = {}; + configuredSession.hostRect = XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f); + configuredSession.canvasRect = XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f); + configuredSession.validCanvas = true; + host.SetCanvasSession(configuredSession); + + XCUIPanelCanvasRequest request = {}; + request.childId = "NativeSurfaceCanvas"; + request.showSurfaceImage = true; + request.drawPreviewFrame = true; + request.surfaceImage.texture = MakeSurfaceTextureHandle(17u, 1024u, 512u); + request.surfaceImage.surfaceWidth = 1024u; + request.surfaceImage.surfaceHeight = 512u; + request.surfaceImage.uvMin = XCEngine::UI::UIPoint(0.0f, 0.0f); + request.surfaceImage.uvMax = XCEngine::UI::UIPoint(1.0f, 1.0f); + + const XCUIPanelCanvasSession session = host.BeginCanvas(request); + EXPECT_TRUE(session.validCanvas); + host.EndCanvas(); + + XCUIPanelCanvasFrameSnapshot snapshot = {}; + ASSERT_TRUE(host.TryGetLatestFrameSnapshot(snapshot)); + EXPECT_TRUE(snapshot.showingSurfaceImage); + EXPECT_TRUE(snapshot.surfaceImage.IsValid()); + EXPECT_EQ(snapshot.surfaceImage.texture.nativeHandle, 17u); + EXPECT_EQ(snapshot.overlayDrawData.GetDrawListCount(), 1u); + EXPECT_EQ(snapshot.overlayDrawData.GetTotalCommandCount(), 3u); + + const auto& commands = snapshot.overlayDrawData.GetDrawLists().front().GetCommands(); + ASSERT_EQ(commands.size(), 3u); + EXPECT_EQ(commands[0].type, XCEngine::UI::UIDrawCommandType::PushClipRect); + EXPECT_EQ(commands[1].type, XCEngine::UI::UIDrawCommandType::RectOutline); + EXPECT_EQ(commands[2].type, XCEngine::UI::UIDrawCommandType::PopClipRect); +} + +TEST(NativeXCUIPanelCanvasHostTest, ClearingConfiguredSessionFallsBackToPassiveSnapshot) { + NativeXCUIPanelCanvasHost host = {}; + XCUIPanelCanvasSession configuredSession = {}; + configuredSession.hostRect = XCEngine::UI::UIRect(4.0f, 5.0f, 320.0f, 240.0f); + configuredSession.canvasRect = XCEngine::UI::UIRect(4.0f, 25.0f, 320.0f, 220.0f); + configuredSession.validCanvas = true; + host.SetCanvasSession(configuredSession); + ASSERT_TRUE(host.HasConfiguredSession()); + + host.ClearCanvasSession(); + EXPECT_FALSE(host.HasConfiguredSession()); + + XCUIPanelCanvasRequest request = {}; + request.height = 180.0f; + request.topInset = 24.0f; + const XCUIPanelCanvasSession session = host.BeginCanvas(request); + host.EndCanvas(); + + EXPECT_FALSE(session.validCanvas); + EXPECT_FLOAT_EQ(session.hostRect.height, 180.0f); + EXPECT_FLOAT_EQ(session.canvasRect.y, 24.0f); + EXPECT_FLOAT_EQ(session.canvasRect.height, 156.0f); + + XCUIPanelCanvasFrameSnapshot snapshot = {}; + ASSERT_TRUE(host.TryGetLatestFrameSnapshot(snapshot)); + EXPECT_FALSE(snapshot.session.validCanvas); + EXPECT_EQ(snapshot.overlayDrawData.GetDrawListCount(), 0u); +} + +} // namespace