Default new_editor to native XCUI shell host

This commit is contained in:
2026-04-05 14:05:46 +08:00
parent e22d7d7f3d
commit 551eefbaa1
11 changed files with 1772 additions and 98 deletions

View File

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

View File

@@ -2,13 +2,16 @@
#include "XCUIBackend/ImGuiXCUIPanelCanvasHost.h"
#include "XCUIBackend/ImGuiXCUIHostedPreviewPresenter.h"
#include "XCUIBackend/ImGuiWindowUICompositor.h"
#include "XCUIBackend/NativeWindowUICompositor.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/UI/DrawData.h>
#include <imgui.h>
#include <algorithm>
#include <cmath>
#include <sstream>
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 <typename ResourceType>
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::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
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<std::int32_t>& 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<std::int32_t>(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<XCUIDemoPanel>(
&m_xcuiInputSource,
CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo)),
::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIPanelCanvasHost());
m_layoutLabPanel = std::make_unique<XCUILayoutLabPanel>(
&m_xcuiInputSource,
CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab)),
::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIPanelCanvasHost());
ConfigureHostedPreviewPresenters();
if (IsNativeWindowHostEnabled()) {
InitializeNativeShell();
} else {
m_demoPanel = std::make_unique<XCUIDemoPanel>(
&m_xcuiInputSource,
CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo)),
::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIPanelCanvasHost());
m_layoutLabPanel = std::make_unique<XCUILayoutLabPanel>(
&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<float>(clientRect.right - clientRect.left);
const float windowHeight = static_cast<float>(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<NativeShellPanelLayout> 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<NativePanelFrameSummary> 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<unsigned long long>(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<float>(
std::chrono::duration<double>(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<float>(surface.GetWidth());
const float height = static_cast<float>(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<float>(
std::chrono::duration<double>(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<float>(
std::chrono::duration<double>(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<float>(surface.GetWidth());
const float height = static_cast<float>(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

View File

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

View File

@@ -0,0 +1,218 @@
#include "XCUIBackend/NativeWindowUICompositor.h"
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/RHI/RHICommandList.h>
#include <XCEngine/RHI/RHICommandQueue.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/RHI/RHISwapChain.h>
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<IWindowUICompositor> CreateNativeWindowUICompositor() {
return std::make_unique<NativeWindowUICompositor>();
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,76 @@
#pragma once
#include "XCUIBackend/IWindowUICompositor.h"
#include "XCUIBackend/IXCUITextAtlasProvider.h"
#include "XCUIBackend/XCUIRHIRenderBackend.h"
#include <XCEngine/UI/DrawData.h>
#include <cstddef>
#include <memory>
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

View File

@@ -0,0 +1,294 @@
#pragma once
#include "XCUIBackend/XCUIPanelCanvasHost.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
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<IXCUIPanelCanvasHost> CreateNativeXCUIPanelCanvasHost() {
return std::make_unique<NativeXCUIPanelCanvasHost>();
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -2,8 +2,6 @@
#include <algorithm>
#include <chrono>
#include <cmath>
#include <limits>
namespace XCEngine {
namespace Editor {
@@ -64,13 +62,6 @@ void AppendUniqueKeyCode(std::vector<std::int32_t>& keyCodes, std::int32_t keyCo
}
}
bool IsPointerPositionValid(const ImVec2& position) {
return std::isfinite(position.x) &&
std::isfinite(position.y) &&
position.x > -std::numeric_limits<float>::max() * 0.5f &&
position.y > -std::numeric_limits<float>::max() * 0.5f;
}
std::uint64_t GetTimestampNanoseconds() {
return static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(

View File

@@ -3,8 +3,6 @@
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/Types.h>
#include <imgui.h>
#include <array>
#include <cstddef>
#include <cstdint>

View File

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

View File

@@ -0,0 +1,212 @@
#include <gtest/gtest.h>
#include "XCUIBackend/IWindowUICompositor.h"
#include "XCUIBackend/NativeWindowUICompositor.h"
#include "XCUIBackend/UITextureRegistration.h"
#include <XCEngine/UI/DrawData.h>
#include <array>
#include <cstdint>
#include <memory>
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<HWND>(static_cast<std::uintptr_t>(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<float, 4>{ 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<IWindowUICompositor> 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<float, 4>{ 0.0f, 0.0f, 0.0f, 1.0f }.data(),
[&uiRendered]() { uiRendered = true; },
{},
{});
EXPECT_FALSE(uiRendered);
compositor->Shutdown();
}
} // namespace

View File

@@ -0,0 +1,176 @@
#include <gtest/gtest.h>
#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<IXCUIPanelCanvasHost> 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