Integrate XCUI shell state and runtime frame seams

This commit is contained in:
2026-04-05 12:50:55 +08:00
parent ec97445071
commit e5e9f348a3
29 changed files with 3183 additions and 102 deletions

View File

@@ -53,6 +53,8 @@ Current gap:
- `UIScreenStackController::ReplaceTop` now preserves the previous top screen if the replacement screen fails to load, so runtime menu flows do not silently drop their active layer on bad assets.
- `SceneRuntime` now owns a dedicated `UISceneRuntimeContext`, so game/runtime code has a first-class place to configure viewport/focus, queue `UIInputEvent`s, drive `UISystem` each `Update`, and inspect the latest UI frame result.
- Runtime screen emission now also carries concrete button text in the shared document host path instead of silently dropping button labels.
- `UISystemFrameResult` now also preserves viewport rect, submitted input count, frame delta, and focus state, and both `UISystem` and `UISceneRuntimeContext` now expose `ConsumeLastFrame()` so runtime/game hosts can drain the last retained frame packet without copying editor-side concepts into the shared layer.
- `UIScreenPlayer` now also exposes `ConsumeLastFrame()`, so player/system/context all share the same consume-vs-borrow frame ownership semantics in the runtime layer.
Current gap:
@@ -75,32 +77,42 @@ Current gap:
- `new_editor` now also has an isolated `XCUIEditorCommandRouter` model with shortcut matching, enable predicates, and direct command invocation semantics covered by dedicated tests, ready for shell-frame integration.
- `XCUI Demo` now exports pending per-frame command ids through `DrainPendingCommandIds()`, so editor-side hosts have a clean seam for observing demo/runtime command traffic without parsing draw data.
- `XCUI Demo` and `LayoutLab` panel canvases are now being pulled behind a dedicated `IXCUIPanelCanvasHost` seam, so canvas surface presentation, hover/focus fallback state, and overlay draw hooks no longer have to stay hard-coded inside each ImGui panel implementation.
- The panel-canvas seam now also exposes explicit backend/capability metadata and a minimal `NullXCUIPanelCanvasHost`, so non-ImGui host paths have a concrete placeholder backend instead of relying on an implicit ImGui default.
- Panel diagnostics were expanded to clearly separate preview/runtime/input state and native vs legacy paths.
- The editor bridge layer now has smoke coverage for swapchain after-UI rendering hooks and SRV-backed ImGui texture descriptor registration.
- `Application` no longer owns the ImGui backend directly; window presentation now routes through `IWindowUICompositor` with an `ImGuiWindowUICompositor` implementation, which currently delegates to `IEditorHostCompositor` / `ImGuiHostCompositor`.
- Hosted preview offscreen surfaces now keep compositor-returned `UITextureRegistration` / `UITextureHandle` data inside `Application` instead of storing `ImTextureID` directly.
- The generic hosted-preview presenter contract no longer owns `ImGuiTransitionBackend`; the ImGui presenter now sits in a separate `ImGuiXCUIHostedPreviewPresenter` header while the native queue/surface registry remains XCUI-generic.
- The generic hosted-preview frame contract no longer carries an ImGui draw-list pointer; the legacy ImGui presenter resolves its inline draw target from the active ImGui window context instead of pushing that type through the XCUI contract.
- The legacy ImGui hosted-preview presenter now also accepts an explicit draw-target binding object, so presenter-side `ImGui::GetWindowDrawList()` lookup is no longer hard-coded inside the generic presenter path and can stay isolated behind the ImGui adapter layer.
- `Application` shell menu toggles and global shortcuts now route through `XCUIEditorCommandRouter` instead of directly mutating shell booleans from menu callbacks, giving the editor layer a real command-routing seam.
- `LayoutLab` runtime now consumes the shared `UIKeyboardNavigationModel` for abstract list/tree/property navigation actions (`previous/next/home/end/collapse/expand`), so keyboard collection traversal rules are no longer trapped in sandbox-local state.
- `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.
- `XCNewEditor` builds successfully to `build/new_editor/bin/Debug/XCNewEditor.exe`.
Current gap:
- The shell is still ImGui-hosted.
- Legacy hosted preview still depends on an active ImGui window context for inline presentation.
- Legacy hosted preview still depends on an ImGui-specific inline draw target binding for presentation.
- The new panel-canvas seam still only has an ImGui adapter today; a native panel/shell host still needs to replace it before ImGui can stop being the default editor host path.
- 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 the new keyboard-navigation/property-edit/command-routing models are still only partially integrated.
- 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 panel-level keyboard mapping plus shell-state adoption are still only partially integrated.
## Validated This Phase
- `new_editor_xcui_demo_runtime_tests`: `7/7`
- `new_editor_xcui_layout_lab_runtime_tests`: `9/9`
- `new_editor_xcui_demo_runtime_tests`: `12/12`
- `new_editor_xcui_layout_lab_runtime_tests`: `12/12`
- `new_editor_xcui_rhi_command_compiler_tests`: `6/6`
- `new_editor_xcui_rhi_render_backend_tests`: `5/5`
- `new_editor_xcui_hosted_preview_presenter_tests`: `14/14`
- `new_editor_xcui_hosted_preview_presenter_tests`: `17/17`
- `new_editor_imgui_window_ui_compositor_tests`: `7/7`
- `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`: `8/8`
- `new_editor_xcui_panel_canvas_host_tests`: `2/2`
- `new_editor_imgui_xcui_panel_canvas_host_tests`: `1/1`
- `XCNewEditor` Debug target builds successfully
- `core_ui_tests`: `49 total` (`47` passed, `2` skipped because `KeyCode::Delete` currently aliases `Backspace`)
- `core_ui_tests`: `52 total` (`50` passed, `2` skipped because `KeyCode::Delete` currently aliases `Backspace`)
- `scene_tests`: `68/68`
- `core_ui_style_tests`: `5/5`
- `ui_resource_tests`: `11/11`
@@ -117,6 +129,11 @@ Current gap:
- Common-core `UIExpansionModel` extraction now owns reusable expansion/collapse state for tree/property-style widgets, with dedicated `core_ui_tests` coverage.
- Common-core `UIKeyboardNavigationModel` extraction now owns reusable current-index/anchor navigation state for collection-style widgets, with dedicated `core_ui_tests` coverage.
- Common-core `UIPropertyEditModel` extraction now owns reusable property-field edit session state, including staged values and commit/cancel behavior, with dedicated `core_ui_tests` coverage.
- Runtime frame ownership was tightened again:
- `UIScreenPlayer::ConsumeLastFrame()` now exposes consume-style packet ownership at the player layer
- `UISystemFrameResult` now carries viewport rect, submitted input event count, frame delta, and focus state
- `UISystem::ConsumeLastFrame()` moves the retained packet out of the runtime layer
- `UISceneRuntimeContext::ConsumeLastFrame()` forwards the same shared runtime seam upward
- Demo runtime text editing was extended with:
- click-to-place caret
- `Delete` support
@@ -128,6 +145,7 @@ Current gap:
- LayoutLab click-selection now persists through the shared `UISelectionModel`, including selected-state diagnostics and reusable visual selection feedback on cards, collection rows, and field rows.
- LayoutLab tree expansion and property-section collapse now persist through the shared `UIExpansionModel`, including reserved property headers, disclosure glyphs, and runtime coverage for collapsed/expanded visibility.
- `XCUIDemoRuntime` now exposes `DrainPendingCommandIds()` so hosts can observe emitted runtime commands in order across pointer/text interactions without scraping UI text or draw-command payloads.
- `XCUIDemoRuntime` command recording was tightened so pointer activation, text editing, and shortcut-triggered commands now share one bridge path and preserve mixed ordering in `DrainPendingCommandIds()`.
- Schema document support extended with:
- retained `UISchemaDefinition` data on `UIDocumentModel`
- artifact schema version bump for UI documents
@@ -174,7 +192,14 @@ Current gap:
- The window-level XCUI compositor seam now also has a dedicated regression target around `ImGuiWindowUICompositor`, covering initialization, render-frame ordering, Win32 message forwarding, texture registration forwarding, and shutdown safety.
- The window compositor and hosted-preview seams gained more edge-case coverage around no-UI render passes, compositor re-initialization/rebinding, partial logical-size fallback, and descriptor reuse for repeated queued-frame keys.
- `new_editor` now has a dedicated `XCUIEditorCommandRouter` test target covering command registration, replacement, enable predicates, accelerator matching, and policy gates around focus/keyboard capture/text input.
- `Application` now integrates `XCUIEditorCommandRouter` into the shell itself:
- `View` menu items invoke routed commands instead of directly mutating shell state
- shell-level shortcuts now flow from `XCUIWin32InputSource` through `XCUIInputBridge` into command matching
- hosted-preview mode toggles still trigger presenter reconfiguration through the routed command bindings
- `new_editor` panel canvas ownership is now being split behind `IXCUIPanelCanvasHost`, with an `ImGuiXCUIPanelCanvasHost` adapter carrying the legacy path so panel code stops directly owning `ImGui::Image` / `ImGui::InvisibleButton` / draw-list preview plumbing.
- `new_editor` now also has a pure `XCUIShellChromeState` model with dedicated tests, covering shell panel visibility, hosted-preview mode, and shell view toggles without depending on ImGui or `Application`.
- `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.
- 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.
- `SceneRuntime` layered XCUI routing now has dedicated regression coverage for:
- top-interactive layer input ownership
- blocking/modal layer suppression of lower layers
@@ -185,8 +210,10 @@ Current gap:
- generic preview surface metadata stays on XCUI-owned value types
- `ImGuiTransitionBackend` moved behind `ImGuiXCUIHostedPreviewPresenter`
- generic preview frame submission no longer carries an ImGui draw-list pointer
- the ImGui presenter now resolves inline draw targets through an explicit ImGui-only binding seam
- panel/runtime callers still preserve the same legacy and native-preview behavior
- `LayoutLab` now resolves editor collection widget taxonomy and metrics through shared `UIEditorCollectionPrimitives` helpers instead of duplicating the same tag and metric rules inside the sandbox runtime.
- `LayoutLab` runtime now consumes shared keyboard-navigation semantics for list/tree/property traversal, while the remaining panel-level key mapping is tracked as an editor-host integration gap rather than a runtime gap.
- `new_editor` panel/shell diagnostics improvements for hosted preview state.
- XCUI asset document loading changed to prefer direct source compilation before `ResourceManager` fallback for the sandbox path, fixing the LayoutLab crash.
- `UIDocumentCompiler.cpp` repaired enough to restore full local builds after the duplicated schema-helper regression.
@@ -198,12 +225,13 @@ Current gap:
- `ScrollView` is still authored/static; no wheel-driven scrolling or virtualization yet.
- `Image` widgets still do not have source-rect/atlas-subregion level API in the high-level draw command model.
- Editor shell still depends on ImGui as host chrome.
- Legacy hosted preview still depends on an ImGui-only inline presenter path when not using the queued native surface path.
- Editor widget coverage is still prototype-driven inside `LayoutLab`; it has not yet been promoted into a full reusable shared widget/runtime layer with command routing, virtualization, and property-edit transactions.
## Next Phase
1. Expand runtime/game-layer ownership from the current `SceneRuntime` UI context into scene-declared HUD/menu bootstrapping, draw submission, and higher-level runtime UI policies.
2. Promote the current editor-facing widget prototypes out of authored `LayoutLab` content and into reusable XCUI widget/runtime modules, then continue with toolbar/menu and more native shell-owned chrome.
2. Promote the current editor-facing widget prototypes out of authored `LayoutLab` content and into reusable XCUI widget/runtime modules, then continue with toolbar/menu chrome, shell-state adoption, and panel-level keyboard/navigation input plumbing.
3. Add a native XCUI host compositor on the existing window-level compositor seam so `new_editor` can present without going through ImGui-owned draw data.
4. Reduce remaining ImGui leakage in hosted preview surfaces and panel contracts now that the compositor seam is in place.
4. Replace the remaining ImGui-only fallback seams in hosted preview and panel canvas hosting with native host implementations so ImGui can become compatibility-only instead of the default shell path.
5. Continue phased validation, commit, push, and plan refresh after each stable batch.

View File

@@ -19,6 +19,7 @@ public:
const UIScreenStackController& GetStackController() const;
const UISystemFrameResult& GetLastFrame() const;
UISystemFrameResult ConsumeLastFrame();
void Reset();
void SetViewportRect(const UIRect& viewportRect);

View File

@@ -20,6 +20,7 @@ public:
const UIScreenAsset* GetAsset() const;
const UIScreenDocument* GetDocument() const;
const UIScreenFrameResult& GetLastFrame() const;
UIScreenFrameResult ConsumeLastFrame();
const std::string& GetLastError() const;
std::uint64_t GetPresentedFrameCount() const;

View File

@@ -89,9 +89,13 @@ struct UISystemPresentedLayer {
struct UISystemFrameResult {
UIDrawData drawData = {};
std::vector<UISystemPresentedLayer> layers = {};
UIRect viewportRect = {};
std::size_t presentedLayerCount = 0;
std::size_t skippedLayerCount = 0;
std::size_t submittedInputEventCount = 0;
std::uint64_t frameIndex = 0;
double deltaTimeSeconds = 0.0;
bool focused = false;
std::string errorMessage = {};
};

View File

@@ -31,6 +31,7 @@ public:
const UISystemFrameResult& Update(const UIScreenFrameInput& input);
void Tick(const UIScreenFrameInput& input);
const UISystemFrameResult& GetLastFrame() const;
UISystemFrameResult ConsumeLastFrame();
const std::vector<std::unique_ptr<UIScreenPlayer>>& GetPlayers() const;

View File

@@ -29,6 +29,10 @@ const UISystemFrameResult& UISceneRuntimeContext::GetLastFrame() const {
return m_system.GetLastFrame();
}
UISystemFrameResult UISceneRuntimeContext::ConsumeLastFrame() {
return m_system.ConsumeLastFrame();
}
void UISceneRuntimeContext::Reset() {
m_stackController.Clear();
m_system.DestroyAllPlayers();

View File

@@ -75,6 +75,12 @@ const UIScreenFrameResult& UIScreenPlayer::GetLastFrame() const {
return m_lastFrame;
}
UIScreenFrameResult UIScreenPlayer::ConsumeLastFrame() {
UIScreenFrameResult frame = std::move(m_lastFrame);
m_lastFrame = {};
return frame;
}
const std::string& UIScreenPlayer::GetLastError() const {
return m_lastError;
}

View File

@@ -1,5 +1,7 @@
#include <XCEngine/UI/Runtime/UISystem.h>
#include <utility>
namespace XCEngine {
namespace UI {
namespace Runtime {
@@ -122,6 +124,10 @@ std::size_t UISystem::GetLayerCount() const {
const UISystemFrameResult& UISystem::Update(const UIScreenFrameInput& input) {
m_lastFrame = {};
m_lastFrame.frameIndex = input.frameIndex;
m_lastFrame.viewportRect = input.viewportRect;
m_lastFrame.submittedInputEventCount = input.events.size();
m_lastFrame.deltaTimeSeconds = input.deltaTimeSeconds;
m_lastFrame.focused = input.focused;
if (m_players.empty()) {
return m_lastFrame;
@@ -177,6 +183,12 @@ const UISystemFrameResult& UISystem::GetLastFrame() const {
return m_lastFrame;
}
UISystemFrameResult UISystem::ConsumeLastFrame() {
UISystemFrameResult frame = std::move(m_lastFrame);
m_lastFrame = {};
return frame;
}
const std::vector<std::unique_ptr<UIScreenPlayer>>& UISystem::GetPlayers() const {
return m_players;
}

View File

@@ -1,5 +1,4 @@
#include "Application.h"
#include "XCUIBackend/ImGuiXCUIHostedPreviewPresenter.h"
#include "XCUIBackend/ImGuiWindowUICompositor.h"
@@ -91,16 +90,188 @@ Application::CreateHostedPreviewPresenter(bool nativePreview) {
return ::XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter();
}
void Application::ConfigureHostedPreviewPresenters() {
if (m_demoPanel != nullptr) {
m_demoPanel->SetHostedPreviewEnabled(true);
m_demoPanel->SetHostedPreviewPresenter(CreateHostedPreviewPresenter(m_showNativeDemoPanelPreview));
Application::ShellPanelChromeState* Application::TryGetShellPanelState(ShellPanelId panelId) {
const std::size_t index = GetShellPanelIndex(panelId);
if (index >= m_shellPanels.size()) {
return nullptr;
}
if (m_layoutLabPanel != nullptr) {
m_layoutLabPanel->SetHostedPreviewEnabled(true);
m_layoutLabPanel->SetHostedPreviewPresenter(CreateHostedPreviewPresenter(m_showNativeLayoutLabPreview));
return &m_shellPanels[index];
}
const Application::ShellPanelChromeState* Application::TryGetShellPanelState(ShellPanelId panelId) const {
const std::size_t index = GetShellPanelIndex(panelId);
if (index >= m_shellPanels.size()) {
return nullptr;
}
return &m_shellPanels[index];
}
bool Application::IsShellViewToggleEnabled(ShellViewToggleId toggleId) const {
switch (toggleId) {
case ShellViewToggleId::ImGuiDemoWindow:
return m_shellViewToggles.imguiDemoWindowVisible;
case ShellViewToggleId::NativeBackdrop:
return m_shellViewToggles.nativeBackdropVisible;
case ShellViewToggleId::PulseAccent:
return m_shellViewToggles.pulseAccentEnabled;
case ShellViewToggleId::NativeXCUIOverlay:
return m_shellViewToggles.nativeXCUIOverlayVisible;
case ShellViewToggleId::HostedPreviewHud:
return m_shellViewToggles.hostedPreviewHudVisible;
case ShellViewToggleId::Count:
default:
return false;
}
}
void Application::SetShellViewToggleEnabled(ShellViewToggleId toggleId, bool enabled) {
switch (toggleId) {
case ShellViewToggleId::ImGuiDemoWindow:
m_shellViewToggles.imguiDemoWindowVisible = enabled;
return;
case ShellViewToggleId::NativeBackdrop:
m_shellViewToggles.nativeBackdropVisible = enabled;
return;
case ShellViewToggleId::PulseAccent:
m_shellViewToggles.pulseAccentEnabled = enabled;
return;
case ShellViewToggleId::NativeXCUIOverlay:
m_shellViewToggles.nativeXCUIOverlayVisible = enabled;
return;
case ShellViewToggleId::HostedPreviewHud:
m_shellViewToggles.hostedPreviewHudVisible = enabled;
return;
case ShellViewToggleId::Count:
default:
return;
}
}
bool Application::IsNativeHostedPreviewEnabled(ShellPanelId panelId) const {
const ShellPanelChromeState* panelState = TryGetShellPanelState(panelId);
return panelState != nullptr &&
panelState->hostedPreviewEnabled &&
panelState->previewMode == ShellHostedPreviewMode::NativeOffscreen;
}
void Application::ConfigureHostedPreviewPresenters() {
const ShellPanelChromeState* demoState = TryGetShellPanelState(ShellPanelId::XCUIDemo);
if (m_demoPanel != nullptr) {
m_demoPanel->SetVisible(demoState != nullptr && demoState->visible);
m_demoPanel->SetHostedPreviewEnabled(demoState == nullptr || demoState->hostedPreviewEnabled);
m_demoPanel->SetHostedPreviewPresenter(CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo)));
}
const ShellPanelChromeState* layoutLabState = TryGetShellPanelState(ShellPanelId::XCUILayoutLab);
if (m_layoutLabPanel != nullptr) {
m_layoutLabPanel->SetVisible(layoutLabState != nullptr && layoutLabState->visible);
m_layoutLabPanel->SetHostedPreviewEnabled(layoutLabState == nullptr || layoutLabState->hostedPreviewEnabled);
m_layoutLabPanel->SetHostedPreviewPresenter(
CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab)));
}
}
void Application::ConfigureShellCommandRouter() {
m_shellCommandRouter.Clear();
ShellCommandBindings bindings = {};
bindings.getXCUIDemoPanelVisible = [this]() {
const ShellPanelChromeState* panelState = TryGetShellPanelState(ShellPanelId::XCUIDemo);
return panelState != nullptr && panelState->visible;
};
bindings.setXCUIDemoPanelVisible = [this](bool visible) {
if (ShellPanelChromeState* panelState = TryGetShellPanelState(ShellPanelId::XCUIDemo)) {
panelState->visible = visible;
}
if (m_demoPanel != nullptr) {
m_demoPanel->SetVisible(visible);
}
};
bindings.getXCUILayoutLabPanelVisible = [this]() {
const ShellPanelChromeState* panelState = TryGetShellPanelState(ShellPanelId::XCUILayoutLab);
return panelState != nullptr && panelState->visible;
};
bindings.setXCUILayoutLabPanelVisible = [this](bool visible) {
if (ShellPanelChromeState* panelState = TryGetShellPanelState(ShellPanelId::XCUILayoutLab)) {
panelState->visible = visible;
}
if (m_layoutLabPanel != nullptr) {
m_layoutLabPanel->SetVisible(visible);
}
};
bindings.getImGuiDemoWindowVisible = [this]() {
return IsShellViewToggleEnabled(ShellViewToggleId::ImGuiDemoWindow);
};
bindings.setImGuiDemoWindowVisible = [this](bool visible) {
SetShellViewToggleEnabled(ShellViewToggleId::ImGuiDemoWindow, visible);
};
bindings.getNativeBackdropVisible = [this]() {
return IsShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop);
};
bindings.setNativeBackdropVisible = [this](bool visible) {
SetShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop, visible);
};
bindings.getPulseAccentEnabled = [this]() {
return IsShellViewToggleEnabled(ShellViewToggleId::PulseAccent);
};
bindings.setPulseAccentEnabled = [this](bool enabled) {
SetShellViewToggleEnabled(ShellViewToggleId::PulseAccent, enabled);
};
bindings.getNativeXCUIOverlayVisible = [this]() {
return IsShellViewToggleEnabled(ShellViewToggleId::NativeXCUIOverlay);
};
bindings.setNativeXCUIOverlayVisible = [this](bool visible) {
SetShellViewToggleEnabled(ShellViewToggleId::NativeXCUIOverlay, visible);
};
bindings.getHostedPreviewHudVisible = [this]() {
return IsShellViewToggleEnabled(ShellViewToggleId::HostedPreviewHud);
};
bindings.setHostedPreviewHudVisible = [this](bool visible) {
SetShellViewToggleEnabled(ShellViewToggleId::HostedPreviewHud, visible);
};
bindings.getNativeDemoPanelPreviewEnabled = [this]() {
return IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo);
};
bindings.setNativeDemoPanelPreviewEnabled = [this](bool enabled) {
if (ShellPanelChromeState* panelState = TryGetShellPanelState(ShellPanelId::XCUIDemo)) {
panelState->previewMode =
enabled
? ShellHostedPreviewMode::NativeOffscreen
: ShellHostedPreviewMode::LegacyImGui;
}
};
bindings.getNativeLayoutLabPreviewEnabled = [this]() {
return IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab);
};
bindings.setNativeLayoutLabPreviewEnabled = [this](bool enabled) {
if (ShellPanelChromeState* panelState = TryGetShellPanelState(ShellPanelId::XCUILayoutLab)) {
panelState->previewMode =
enabled
? ShellHostedPreviewMode::NativeOffscreen
: ShellHostedPreviewMode::LegacyImGui;
}
};
bindings.onHostedPreviewModeChanged = [this]() { ConfigureHostedPreviewPresenters(); };
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;
const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta frameDelta =
m_shellInputBridge.Translate(snapshot);
const ::XCEngine::Editor::XCUIBackend::XCUIEditorCommandInputSnapshot commandSnapshot =
Application::BuildShellShortcutSnapshot(frameDelta);
m_shellCommandRouter.InvokeMatchingShortcut({ &commandSnapshot });
}
Application::HostedPreviewPanelDiagnostics Application::BuildHostedPreviewPanelDiagnostics(
@@ -185,10 +356,13 @@ int Application::Run(HINSTANCE instance, int nCmdShow) {
InitializeWindowCompositor();
m_demoPanel = std::make_unique<XCUIDemoPanel>(
&m_xcuiInputSource,
CreateHostedPreviewPresenter(m_showNativeDemoPanelPreview));
CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo)));
m_layoutLabPanel = std::make_unique<XCUILayoutLabPanel>(
&m_xcuiInputSource,
CreateHostedPreviewPresenter(m_showNativeLayoutLabPreview));
CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab)));
ConfigureHostedPreviewPresenters();
m_shellInputBridge.Reset();
ConfigureShellCommandRouter();
m_running = true;
m_renderReady = true;
@@ -356,11 +530,29 @@ void Application::DestroyHostedPreviewSurfaces() {
m_hostedPreviewSurfaces.clear();
}
void Application::SyncShellChromePanelStateFromPanels() {
if (ShellPanelChromeState* demoState = TryGetShellPanelState(ShellPanelId::XCUIDemo)) {
demoState->visible = m_demoPanel != nullptr && m_demoPanel->IsVisible();
}
if (ShellPanelChromeState* layoutLabState = TryGetShellPanelState(ShellPanelId::XCUILayoutLab)) {
layoutLabState->visible = m_layoutLabPanel != nullptr && m_layoutLabPanel->IsVisible();
}
}
void Application::SyncHostedPreviewSurfaces() {
const auto isNativePreviewEnabled = [this](const std::string& debugName) {
return
(debugName == "XCUI Demo" && m_showNativeDemoPanelPreview) ||
(debugName == "XCUI Layout Lab" && m_showNativeLayoutLabPreview);
const ShellPanelChromeState* demoState = TryGetShellPanelState(ShellPanelId::XCUIDemo);
if (demoState != nullptr &&
debugName == demoState->previewDebugName &&
IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo)) {
return true;
}
const ShellPanelChromeState* layoutLabState = TryGetShellPanelState(ShellPanelId::XCUILayoutLab);
return layoutLabState != nullptr &&
debugName == layoutLabState->previewDebugName &&
IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab);
};
const auto syncSurfaceForNameAndSize =
@@ -523,6 +715,8 @@ bool Application::RenderHostedPreviewOffscreenSurface(
}
void Application::RenderShellChrome() {
SyncShellChromePanelStateFromPanels();
ImGuiViewport* viewport = ImGui::GetMainViewport();
if (viewport == nullptr) {
return;
@@ -551,38 +745,62 @@ void Application::RenderShellChrome() {
if (opened) {
if (ImGui::BeginMenuBar()) {
if (ImGui::BeginMenu("View")) {
const bool demoVisible = m_demoPanel != nullptr ? m_demoPanel->IsVisible() : false;
bool demoToggle = demoVisible;
if (ImGui::MenuItem("XCUI Demo", nullptr, &demoToggle) && m_demoPanel != nullptr) {
m_demoPanel->SetVisible(demoToggle);
}
const auto drawCommandMenuItem =
[this](const char* label, const char* shortcut, bool selected, const char* commandId) {
const bool enabled = m_shellCommandRouter.IsCommandEnabled(commandId);
if (ImGui::MenuItem(label, shortcut, selected, enabled)) {
m_shellCommandRouter.InvokeCommand(commandId);
}
};
const bool layoutLabVisible =
m_layoutLabPanel != nullptr ? m_layoutLabPanel->IsVisible() : false;
bool layoutLabToggle = layoutLabVisible;
if (ImGui::MenuItem("XCUI Layout Lab", nullptr, &layoutLabToggle) &&
m_layoutLabPanel != nullptr) {
m_layoutLabPanel->SetVisible(layoutLabToggle);
}
ImGui::MenuItem("ImGui Demo", nullptr, &m_showImGuiDemoWindow);
drawCommandMenuItem(
"XCUI Demo",
"Ctrl+1",
TryGetShellPanelState(ShellPanelId::XCUIDemo) != nullptr &&
TryGetShellPanelState(ShellPanelId::XCUIDemo)->visible,
ShellCommandIds::ToggleXCUIDemoPanel);
drawCommandMenuItem(
"XCUI Layout Lab",
"Ctrl+2",
TryGetShellPanelState(ShellPanelId::XCUILayoutLab) != nullptr &&
TryGetShellPanelState(ShellPanelId::XCUILayoutLab)->visible,
ShellCommandIds::ToggleXCUILayoutLabPanel);
drawCommandMenuItem(
"ImGui Demo",
"Ctrl+3",
IsShellViewToggleEnabled(ShellViewToggleId::ImGuiDemoWindow),
ShellCommandIds::ToggleImGuiDemoWindow);
ImGui::Separator();
ImGui::MenuItem("Native Backdrop", nullptr, &m_showNativeBackdrop);
ImGui::MenuItem("Pulse Accent", nullptr, &m_pulseNativeBackdropAccent);
ImGui::MenuItem("Native XCUI Overlay", nullptr, &m_showNativeXCUIOverlay);
ImGui::MenuItem("Hosted Preview HUD", nullptr, &m_showHostedPreviewHud);
bool nativeDemoPanelPreview = m_showNativeDemoPanelPreview;
if (ImGui::MenuItem("Native Demo Panel Preview", nullptr, &nativeDemoPanelPreview) &&
nativeDemoPanelPreview != m_showNativeDemoPanelPreview) {
m_showNativeDemoPanelPreview = nativeDemoPanelPreview;
ConfigureHostedPreviewPresenters();
}
bool nativeLayoutLabPreview = m_showNativeLayoutLabPreview;
if (ImGui::MenuItem("Native Layout Lab Preview", nullptr, &nativeLayoutLabPreview) &&
nativeLayoutLabPreview != m_showNativeLayoutLabPreview) {
m_showNativeLayoutLabPreview = nativeLayoutLabPreview;
ConfigureHostedPreviewPresenters();
}
drawCommandMenuItem(
"Native Backdrop",
"Ctrl+Shift+B",
IsShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop),
ShellCommandIds::ToggleNativeBackdrop);
drawCommandMenuItem(
"Pulse Accent",
"Ctrl+Shift+P",
IsShellViewToggleEnabled(ShellViewToggleId::PulseAccent),
ShellCommandIds::TogglePulseAccent);
drawCommandMenuItem(
"Native XCUI Overlay",
"Ctrl+Shift+O",
IsShellViewToggleEnabled(ShellViewToggleId::NativeXCUIOverlay),
ShellCommandIds::ToggleNativeXCUIOverlay);
drawCommandMenuItem(
"Hosted Preview HUD",
"Ctrl+Shift+H",
IsShellViewToggleEnabled(ShellViewToggleId::HostedPreviewHud),
ShellCommandIds::ToggleHostedPreviewHud);
drawCommandMenuItem(
"Native Demo Panel Preview",
"Ctrl+Alt+1",
IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo),
ShellCommandIds::ToggleNativeDemoPanelPreview);
drawCommandMenuItem(
"Native Layout Lab Preview",
"Ctrl+Alt+2",
IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab),
ShellCommandIds::ToggleNativeLayoutLabPreview);
ImGui::EndMenu();
}
@@ -593,7 +811,7 @@ void Application::RenderShellChrome() {
m_nativeOverlayRuntime.GetFrameResult().stats;
const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewDrainStats& hostedPreviewStats =
m_hostedPreviewQueue.GetLastDrainStats();
if (m_showNativeXCUIOverlay) {
if (IsShellViewToggleEnabled(ShellViewToggleId::NativeXCUIOverlay)) {
ImGui::TextDisabled(
"Native XCUI overlay: %s | runtime %zu cmds (%zu fill, %zu outline, %zu text, %zu image, clips %zu/%zu)",
overlayFrameStats.nativeOverlayReady ? "preflight OK" : "preflight issues",
@@ -616,7 +834,7 @@ void Application::RenderShellChrome() {
nativeOverlayStats.skippedCommandCount);
} else {
ImGui::TextDisabled(
m_showNativeBackdrop
IsShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop)
? "Transition backend + runtime diagnostics + native backbuffer pass"
: "Transition backend + runtime diagnostics");
}
@@ -644,12 +862,16 @@ void Application::RenderShellChrome() {
if (m_demoPanel != nullptr) {
ImGui::TextDisabled(
"XCUI Demo preview: %s",
m_showNativeDemoPanelPreview ? "native offscreen preview surface" : "ImGui hosted preview");
IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo)
? "native offscreen preview surface"
: "ImGui hosted preview");
}
if (m_layoutLabPanel != nullptr) {
ImGui::TextDisabled(
"Layout Lab preview: %s",
m_showNativeLayoutLabPreview ? "native offscreen preview surface" : "ImGui hosted preview");
IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab)
? "native offscreen preview surface"
: "ImGui hosted preview");
}
ImGui::EndMenuBar();
}
@@ -662,7 +884,7 @@ void Application::RenderShellChrome() {
ImGui::End();
if (m_showHostedPreviewHud) {
if (IsShellViewToggleEnabled(ShellViewToggleId::HostedPreviewHud)) {
RenderHostedPreviewHud();
}
}
@@ -673,22 +895,24 @@ void Application::RenderHostedPreviewHud() {
return;
}
const ShellPanelChromeState* demoState = TryGetShellPanelState(ShellPanelId::XCUIDemo);
const ShellPanelChromeState* layoutLabState = TryGetShellPanelState(ShellPanelId::XCUILayoutLab);
const HostedPreviewPanelDiagnostics demoDiagnostics = BuildHostedPreviewPanelDiagnostics(
"XCUI Demo",
"new_editor.panels.xcui_demo",
m_demoPanel != nullptr && m_demoPanel->IsVisible(),
m_demoPanel != nullptr && m_demoPanel->IsHostedPreviewEnabled(),
m_showNativeDemoPanelPreview,
demoState != nullptr ? demoState->previewDebugName.data() : "XCUI Demo",
demoState != nullptr ? demoState->previewDebugSource.data() : "new_editor.panels.xcui_demo",
demoState != nullptr && demoState->visible,
demoState != nullptr && demoState->hostedPreviewEnabled,
IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo),
m_demoPanel != nullptr && m_demoPanel->IsUsingNativeHostedPreview(),
m_demoPanel != nullptr
? m_demoPanel->GetLastPreviewStats()
: ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats{});
const HostedPreviewPanelDiagnostics layoutLabDiagnostics = BuildHostedPreviewPanelDiagnostics(
"XCUI Layout Lab",
"new_editor.panels.xcui_layout_lab",
m_layoutLabPanel != nullptr && m_layoutLabPanel->IsVisible(),
m_layoutLabPanel != nullptr && m_layoutLabPanel->IsHostedPreviewEnabled(),
m_showNativeLayoutLabPreview,
layoutLabState != nullptr ? layoutLabState->previewDebugName.data() : "XCUI Layout Lab",
layoutLabState != nullptr ? layoutLabState->previewDebugSource.data() : "new_editor.panels.xcui_layout_lab",
layoutLabState != nullptr && layoutLabState->visible,
layoutLabState != nullptr && layoutLabState->hostedPreviewEnabled,
IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab),
m_layoutLabPanel != nullptr && m_layoutLabPanel->IsUsingNativeHostedPreview(),
m_layoutLabPanel != nullptr
? m_layoutLabPanel->GetLastPreviewStats()
@@ -870,6 +1094,7 @@ void Application::Frame() {
m_windowCompositor->RenderFrame(
kClearColor,
[this]() {
DispatchShellShortcuts();
RenderShellChrome();
if (m_demoPanel) {
m_demoPanel->RenderIfVisible();
@@ -877,10 +1102,11 @@ void Application::Frame() {
if (m_layoutLabPanel) {
m_layoutLabPanel->RenderIfVisible();
}
if (m_showImGuiDemoWindow) {
ImGui::ShowDemoWindow(&m_showImGuiDemoWindow);
if (m_shellViewToggles.imguiDemoWindowVisible) {
ImGui::ShowDemoWindow(&m_shellViewToggles.imguiDemoWindowVisible);
}
SyncShellChromePanelStateFromPanels();
SyncHostedPreviewSurfaces();
},
[this](
@@ -888,16 +1114,17 @@ void Application::Frame() {
const ::XCEngine::Rendering::RenderSurface& surface) {
RenderQueuedHostedPreviews(renderContext, surface);
if (!m_showNativeBackdrop && !m_showNativeXCUIOverlay) {
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 = m_pulseNativeBackdropAccent;
frameState.drawBackdrop = m_showNativeBackdrop;
if (m_showNativeXCUIOverlay) {
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);

View File

@@ -1,5 +1,8 @@
#pragma once
#include <XCEngine/Input/InputTypes.h>
#include "XCUIBackend/XCUIEditorCommandRouter.h"
#include "panels/XCUIDemoPanel.h"
#include "panels/XCUILayoutLabPanel.h"
@@ -10,10 +13,14 @@
#include "XCUIBackend/XCUIInputBridge.h"
#include "XCUIBackend/XCUILayoutLabRuntime.h"
#include "XCUIBackend/XCUIRHIRenderBackend.h"
#include "XCUIBackend/XCUIShellChromeState.h"
#include "XCUIBackend/XCUIStandaloneTextAtlasProvider.h"
#include <array>
#include <chrono>
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <memory>
#include <string>
#include <vector>
@@ -24,9 +31,252 @@ namespace NewEditor {
class Application {
public:
using ShellPanelId = ::XCEngine::Editor::XCUIBackend::XCUIShellPanelId;
using ShellViewToggleId = ::XCEngine::Editor::XCUIBackend::XCUIShellViewToggleId;
using ShellHostedPreviewMode = ::XCEngine::Editor::XCUIBackend::XCUIShellHostedPreviewMode;
using ShellPanelChromeState = ::XCEngine::Editor::XCUIBackend::XCUIShellPanelChromeState;
using ShellViewToggleState = ::XCEngine::Editor::XCUIBackend::XCUIShellViewToggleState;
struct ShellCommandIds {
static constexpr const char* ToggleXCUIDemoPanel =
::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleXCUIDemoPanel;
static constexpr const char* ToggleXCUILayoutLabPanel =
::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleXCUILayoutLabPanel;
static constexpr const char* ToggleImGuiDemoWindow =
::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleImGuiDemoWindow;
static constexpr const char* ToggleNativeBackdrop =
::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleNativeBackdrop;
static constexpr const char* TogglePulseAccent =
::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::TogglePulseAccent;
static constexpr const char* ToggleNativeXCUIOverlay =
::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleNativeXCUIOverlay;
static constexpr const char* ToggleHostedPreviewHud =
::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleHostedPreviewHud;
static constexpr const char* ToggleNativeDemoPanelPreview =
::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleNativeDemoPanelPreview;
static constexpr const char* ToggleNativeLayoutLabPreview =
::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleNativeLayoutLabPreview;
};
struct ShellCommandBindings {
std::function<bool()> getXCUIDemoPanelVisible = {};
std::function<void(bool)> setXCUIDemoPanelVisible = {};
std::function<bool()> getXCUILayoutLabPanelVisible = {};
std::function<void(bool)> setXCUILayoutLabPanelVisible = {};
std::function<bool()> getImGuiDemoWindowVisible = {};
std::function<void(bool)> setImGuiDemoWindowVisible = {};
std::function<bool()> getNativeBackdropVisible = {};
std::function<void(bool)> setNativeBackdropVisible = {};
std::function<bool()> getPulseAccentEnabled = {};
std::function<void(bool)> setPulseAccentEnabled = {};
std::function<bool()> getNativeXCUIOverlayVisible = {};
std::function<void(bool)> setNativeXCUIOverlayVisible = {};
std::function<bool()> getHostedPreviewHudVisible = {};
std::function<void(bool)> setHostedPreviewHudVisible = {};
std::function<bool()> getNativeDemoPanelPreviewEnabled = {};
std::function<void(bool)> setNativeDemoPanelPreviewEnabled = {};
std::function<bool()> getNativeLayoutLabPreviewEnabled = {};
std::function<void(bool)> setNativeLayoutLabPreviewEnabled = {};
std::function<void()> onHostedPreviewModeChanged = {};
};
static ::XCEngine::Editor::XCUIBackend::XCUIEditorCommandInputSnapshot BuildShellShortcutSnapshot(
const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta& frameDelta) {
::XCEngine::Editor::XCUIBackend::XCUIEditorCommandInputSnapshot snapshot = {};
snapshot.modifiers = frameDelta.state.modifiers;
snapshot.windowFocused = frameDelta.state.windowFocused;
snapshot.wantCaptureKeyboard = frameDelta.state.wantCaptureKeyboard;
snapshot.wantTextInput = frameDelta.state.wantTextInput;
snapshot.keys.reserve(
frameDelta.keyboard.pressedKeys.size() +
frameDelta.keyboard.repeatedKeys.size());
const auto appendKeyState =
[&snapshot](std::int32_t keyCode, bool repeat) {
for (auto& existing : snapshot.keys) {
if (existing.keyCode != keyCode) {
continue;
}
existing.down = true;
existing.repeat = existing.repeat || repeat;
return;
}
::XCEngine::Editor::XCUIBackend::XCUIEditorCommandKeyState keyState = {};
keyState.keyCode = keyCode;
keyState.down = true;
keyState.repeat = repeat;
snapshot.keys.push_back(keyState);
};
for (std::int32_t keyCode : frameDelta.keyboard.pressedKeys) {
appendKeyState(keyCode, false);
}
for (std::int32_t keyCode : frameDelta.keyboard.repeatedKeys) {
appendKeyState(keyCode, true);
}
return snapshot;
}
static void RegisterShellViewCommands(
::XCEngine::Editor::XCUIBackend::XCUIEditorCommandRouter& router,
const ShellCommandBindings& bindings) {
using ::XCEngine::Editor::XCUIBackend::XCUIEditorCommandAccelerator;
using ::XCEngine::Editor::XCUIBackend::XCUIEditorCommandDefinition;
using ::XCEngine::Input::KeyCode;
using ModifierState = ::XCEngine::UI::UIInputModifiers;
const auto bindToggleCommand =
[&router](
const char* commandId,
const std::function<bool()>& getter,
const std::function<void(bool)>& setter,
std::initializer_list<XCUIEditorCommandAccelerator> accelerators,
const std::function<void()>& afterToggle = {}) {
if (!getter || !setter) {
return;
}
XCUIEditorCommandDefinition definition = {};
definition.commandId = commandId;
definition.isEnabled = [getter, setter]() {
return static_cast<bool>(getter) && static_cast<bool>(setter);
};
definition.invoke = [getter, setter, afterToggle]() {
const bool nextValue = !getter();
setter(nextValue);
if (afterToggle) {
afterToggle();
}
};
definition.accelerators.assign(accelerators.begin(), accelerators.end());
router.RegisterCommand(definition);
};
const ModifierState ctrlOnly = { false, true, false, false };
const ModifierState ctrlShift = { true, true, false, false };
const ModifierState ctrlAlt = { false, true, true, false };
bindToggleCommand(
ShellCommandIds::ToggleXCUIDemoPanel,
bindings.getXCUIDemoPanelVisible,
bindings.setXCUIDemoPanelVisible,
{ XCUIEditorCommandAccelerator{
static_cast<std::int32_t>(KeyCode::One),
ctrlOnly,
true,
false } });
bindToggleCommand(
ShellCommandIds::ToggleXCUILayoutLabPanel,
bindings.getXCUILayoutLabPanelVisible,
bindings.setXCUILayoutLabPanelVisible,
{ XCUIEditorCommandAccelerator{
static_cast<std::int32_t>(KeyCode::Two),
ctrlOnly,
true,
false } });
bindToggleCommand(
ShellCommandIds::ToggleImGuiDemoWindow,
bindings.getImGuiDemoWindowVisible,
bindings.setImGuiDemoWindowVisible,
{ XCUIEditorCommandAccelerator{
static_cast<std::int32_t>(KeyCode::Three),
ctrlOnly,
true,
false } });
bindToggleCommand(
ShellCommandIds::ToggleNativeBackdrop,
bindings.getNativeBackdropVisible,
bindings.setNativeBackdropVisible,
{ XCUIEditorCommandAccelerator{
static_cast<std::int32_t>(KeyCode::B),
ctrlShift,
true,
false } });
bindToggleCommand(
ShellCommandIds::TogglePulseAccent,
bindings.getPulseAccentEnabled,
bindings.setPulseAccentEnabled,
{ XCUIEditorCommandAccelerator{
static_cast<std::int32_t>(KeyCode::P),
ctrlShift,
true,
false } });
bindToggleCommand(
ShellCommandIds::ToggleNativeXCUIOverlay,
bindings.getNativeXCUIOverlayVisible,
bindings.setNativeXCUIOverlayVisible,
{ XCUIEditorCommandAccelerator{
static_cast<std::int32_t>(KeyCode::O),
ctrlShift,
true,
false } });
bindToggleCommand(
ShellCommandIds::ToggleHostedPreviewHud,
bindings.getHostedPreviewHudVisible,
bindings.setHostedPreviewHudVisible,
{ XCUIEditorCommandAccelerator{
static_cast<std::int32_t>(KeyCode::H),
ctrlShift,
true,
false } });
bindToggleCommand(
ShellCommandIds::ToggleNativeDemoPanelPreview,
bindings.getNativeDemoPanelPreviewEnabled,
bindings.setNativeDemoPanelPreviewEnabled,
{ XCUIEditorCommandAccelerator{
static_cast<std::int32_t>(KeyCode::One),
ctrlAlt,
true,
false } },
bindings.onHostedPreviewModeChanged);
bindToggleCommand(
ShellCommandIds::ToggleNativeLayoutLabPreview,
bindings.getNativeLayoutLabPreviewEnabled,
bindings.setNativeLayoutLabPreviewEnabled,
{ XCUIEditorCommandAccelerator{
static_cast<std::int32_t>(KeyCode::Two),
ctrlAlt,
true,
false } },
bindings.onHostedPreviewModeChanged);
}
int Run(HINSTANCE instance, int nCmdShow);
private:
using ShellPanelStateArray = std::array<ShellPanelChromeState, static_cast<std::size_t>(ShellPanelId::Count)>;
static constexpr std::size_t GetShellPanelIndex(ShellPanelId panelId) {
return static_cast<std::size_t>(panelId);
}
static ShellPanelStateArray CreateDefaultShellPanelStates() {
ShellPanelStateArray panels = {};
panels[GetShellPanelIndex(ShellPanelId::XCUIDemo)] = {
ShellPanelId::XCUIDemo,
"XCUI Demo",
"XCUI Demo",
"new_editor.panels.xcui_demo",
true,
true,
ShellHostedPreviewMode::NativeOffscreen
};
panels[GetShellPanelIndex(ShellPanelId::XCUILayoutLab)] = {
ShellPanelId::XCUILayoutLab,
"XCUI Layout Lab",
"XCUI Layout Lab",
"new_editor.panels.xcui_layout_lab",
true,
true,
ShellHostedPreviewMode::LegacyImGui
};
return panels;
}
struct HostedPreviewPanelDiagnostics {
std::string debugName = {};
std::string debugSource = {};
@@ -79,10 +329,16 @@ private:
void ShutdownWindowCompositor();
void ShutdownRenderer();
void DestroyHostedPreviewSurfaces();
void SyncShellChromePanelStateFromPanels();
void SyncHostedPreviewSurfaces();
std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> CreateHostedPreviewPresenter(
bool nativePreview);
void ConfigureHostedPreviewPresenters();
ShellPanelChromeState* TryGetShellPanelState(ShellPanelId panelId);
const ShellPanelChromeState* TryGetShellPanelState(ShellPanelId panelId) const;
bool IsShellViewToggleEnabled(ShellViewToggleId toggleId) const;
void SetShellViewToggleEnabled(ShellViewToggleId toggleId, bool enabled);
bool IsNativeHostedPreviewEnabled(ShellPanelId panelId) const;
HostedPreviewPanelDiagnostics BuildHostedPreviewPanelDiagnostics(
const char* debugName,
const char* fallbackDebugSource,
@@ -102,6 +358,8 @@ private:
HostedPreviewOffscreenSurface& previewSurface,
const ::XCEngine::Rendering::RenderContext& renderContext,
const ::XCEngine::UI::UIDrawData& drawData);
void ConfigureShellCommandRouter();
void DispatchShellShortcuts();
void RenderShellChrome();
void RenderHostedPreviewHud();
void RenderQueuedHostedPreviews(
@@ -115,20 +373,17 @@ private:
std::unique_ptr<XCUIDemoPanel> m_demoPanel;
std::unique_ptr<XCUILayoutLabPanel> m_layoutLabPanel;
::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource m_xcuiInputSource;
::XCEngine::Editor::XCUIBackend::XCUIInputBridge m_shellInputBridge;
::XCEngine::Editor::XCUIBackend::XCUIEditorCommandRouter m_shellCommandRouter;
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewQueue m_hostedPreviewQueue;
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceRegistry m_hostedPreviewSurfaceRegistry;
::XCEngine::Editor::XCUIBackend::XCUIStandaloneTextAtlasProvider m_hostedPreviewTextAtlasProvider;
::XCEngine::Editor::XCUIBackend::XCUIRHIRenderBackend m_hostedPreviewRenderBackend;
ShellViewToggleState m_shellViewToggles = {};
ShellPanelStateArray m_shellPanels = CreateDefaultShellPanelStates();
std::vector<HostedPreviewOffscreenSurface> m_hostedPreviewSurfaces = {};
::XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime m_nativeOverlayRuntime;
MainWindowNativeBackdropRenderer m_nativeBackdropRenderer;
bool m_showImGuiDemoWindow = false;
bool m_showNativeBackdrop = true;
bool m_pulseNativeBackdropAccent = true;
bool m_showNativeXCUIOverlay = true;
bool m_showHostedPreviewHud = true;
bool m_showNativeDemoPanelPreview = true;
bool m_showNativeLayoutLabPreview = false;
bool m_running = false;
bool m_renderReady = false;
std::chrono::steady_clock::time_point m_startTime = {};

View File

@@ -11,8 +11,32 @@ namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
class IImGuiXCUIHostedPreviewTargetBinding {
public:
virtual ~IImGuiXCUIHostedPreviewTargetBinding() = default;
virtual ImDrawList* ResolveTargetDrawList(const XCUIHostedPreviewFrame& frame) const = 0;
};
class ImGuiCurrentWindowXCUIHostedPreviewTargetBinding final
: public IImGuiXCUIHostedPreviewTargetBinding {
public:
ImDrawList* ResolveTargetDrawList(const XCUIHostedPreviewFrame& frame) const override {
(void)frame;
return ImGui::GetWindowDrawList();
}
};
class ImGuiXCUIHostedPreviewPresenter final : public IXCUIHostedPreviewPresenter {
public:
explicit ImGuiXCUIHostedPreviewPresenter(
std::unique_ptr<IImGuiXCUIHostedPreviewTargetBinding> targetBinding = {})
: m_targetBinding(std::move(targetBinding)) {
if (m_targetBinding == nullptr) {
m_targetBinding = std::make_unique<ImGuiCurrentWindowXCUIHostedPreviewTargetBinding>();
}
}
bool Present(const XCUIHostedPreviewFrame& frame) override {
m_lastStats = {};
if (frame.drawData == nullptr) {
@@ -23,7 +47,14 @@ public:
m_backend.Submit(*frame.drawData);
m_lastStats.submittedDrawListCount = m_backend.GetPendingDrawListCount();
m_lastStats.submittedCommandCount = m_backend.GetPendingCommandCount();
m_lastStats.presented = m_backend.EndFrame(ImGui::GetWindowDrawList());
ImDrawList* targetDrawList =
m_targetBinding != nullptr ? m_targetBinding->ResolveTargetDrawList(frame) : nullptr;
if (targetDrawList == nullptr) {
m_backend.BeginFrame();
return false;
}
m_lastStats.presented = m_backend.EndFrame(targetDrawList);
m_lastStats.flushedDrawListCount = m_backend.GetLastFlushedDrawListCount();
m_lastStats.flushedCommandCount = m_backend.GetLastFlushedCommandCount();
return m_lastStats.presented;
@@ -35,11 +66,22 @@ public:
private:
ImGuiTransitionBackend m_backend = {};
std::unique_ptr<IImGuiXCUIHostedPreviewTargetBinding> m_targetBinding = {};
XCUIHostedPreviewStats m_lastStats = {};
};
inline std::unique_ptr<IImGuiXCUIHostedPreviewTargetBinding>
CreateImGuiCurrentWindowXCUIHostedPreviewTargetBinding() {
return std::make_unique<ImGuiCurrentWindowXCUIHostedPreviewTargetBinding>();
}
inline std::unique_ptr<IXCUIHostedPreviewPresenter> CreateImGuiXCUIHostedPreviewPresenter(
std::unique_ptr<IImGuiXCUIHostedPreviewTargetBinding> targetBinding) {
return std::make_unique<ImGuiXCUIHostedPreviewPresenter>(std::move(targetBinding));
}
inline std::unique_ptr<IXCUIHostedPreviewPresenter> CreateImGuiXCUIHostedPreviewPresenter() {
return std::make_unique<ImGuiXCUIHostedPreviewPresenter>();
return CreateImGuiXCUIHostedPreviewPresenter(CreateImGuiCurrentWindowXCUIHostedPreviewTargetBinding());
}
} // namespace XCUIBackend

View File

@@ -85,6 +85,22 @@ inline void DrawBadge(
class ImGuiXCUIPanelCanvasHost final : public IXCUIPanelCanvasHost {
public:
const char* GetDebugName() const override {
return "ImGuiXCUIPanelCanvasHost";
}
XCUIPanelCanvasHostBackend GetBackend() const override {
return XCUIPanelCanvasHostBackend::ImGui;
}
XCUIPanelCanvasHostCapabilities GetCapabilities() const override {
XCUIPanelCanvasHostCapabilities capabilities = {};
capabilities.supportsPointerHitTesting = true;
capabilities.supportsHostedSurfaceImages = true;
capabilities.supportsPrimitiveOverlays = true;
return capabilities;
}
XCUIPanelCanvasSession BeginCanvas(const XCUIPanelCanvasRequest& request) override {
const char* childId =
request.childId != nullptr && request.childId[0] != '\0'

View File

@@ -0,0 +1,71 @@
#pragma once
#include "XCUIBackend/XCUIPanelCanvasHost.h"
#include <memory>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
class NullXCUIPanelCanvasHost final : public IXCUIPanelCanvasHost {
public:
const char* GetDebugName() const override {
return "NullXCUIPanelCanvasHost";
}
XCUIPanelCanvasHostBackend GetBackend() const override {
return XCUIPanelCanvasHostBackend::Null;
}
XCUIPanelCanvasHostCapabilities GetCapabilities() const override {
return {};
}
XCUIPanelCanvasSession BeginCanvas(const XCUIPanelCanvasRequest& request) override {
(void)request;
return {};
}
void DrawFilledRect(
const ::XCEngine::UI::UIRect& rect,
const ::XCEngine::UI::UIColor& color,
float rounding = 0.0f) override {
(void)rect;
(void)color;
(void)rounding;
}
void DrawOutlineRect(
const ::XCEngine::UI::UIRect& rect,
const ::XCEngine::UI::UIColor& color,
float thickness = 1.0f,
float rounding = 0.0f) override {
(void)rect;
(void)color;
(void)thickness;
(void)rounding;
}
void DrawText(
const ::XCEngine::UI::UIPoint& position,
std::string_view text,
const ::XCEngine::UI::UIColor& color,
float fontSize = 0.0f) override {
(void)position;
(void)text;
(void)color;
(void)fontSize;
}
void EndCanvas() override {
}
};
inline std::unique_ptr<IXCUIPanelCanvasHost> CreateNullXCUIPanelCanvasHost() {
return std::make_unique<NullXCUIPanelCanvasHost>();
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -992,6 +992,41 @@ DemoNode* TryGetNodeByElementId(RuntimeBuildContext& state, UIElementId elementI
return it != state.nodeIndexById.end() ? &state.nodes[it->second] : nullptr;
}
void ApplyActivationEffects(RuntimeBuildContext& state, const DemoNode& node) {
if (IsToggleNode(node)) {
const std::string stateKey = ResolveToggleStateKey(node);
state.toggleStates[stateKey] = !ResolveToggleState(state, node);
}
if (node.actionId == kToggleAccentCommandId || node.elementKey == "toggleAccent") {
state.accentEnabled = !state.accentEnabled;
}
}
bool BridgeCommand(
RuntimeBuildContext& state,
std::string commandId,
UIElementId sourceElementId = 0u) {
if (commandId.empty()) {
return false;
}
if (sourceElementId != 0u) {
if (DemoNode* sourceNode = TryGetNodeByElementId(state, sourceElementId)) {
ApplyActivationEffects(state, *sourceNode);
}
} else if (commandId == kToggleAccentCommandId) {
if (DemoNode* toggleNode = TryGetNodeByElementId(state, state.toggleButtonId)) {
ApplyActivationEffects(state, *toggleNode);
} else {
state.accentEnabled = !state.accentEnabled;
}
}
RecordCommand(state, std::move(commandId));
return true;
}
std::size_t FindCaretOffsetFromPoint(
RuntimeBuildContext& state,
const DemoNode& node,
@@ -1131,16 +1166,7 @@ void ActivateNode(RuntimeBuildContext& state, UIElementId elementId) {
}
const DemoNode& node = state.nodes[it->second];
if (IsToggleNode(node)) {
const std::string stateKey = ResolveToggleStateKey(node);
state.toggleStates[stateKey] = !ResolveToggleState(state, node);
}
if (node.actionId == kToggleAccentCommandId || node.elementKey == "toggleAccent") {
state.accentEnabled = !state.accentEnabled;
}
RecordCommand(state, BuildActivationCommandId(node));
BridgeCommand(state, BuildActivationCommandId(node), elementId);
}
void BuildDemoNodesRecursive(
@@ -2055,10 +2081,10 @@ const XCUIDemoFrameResult& XCUIDemoRuntime::Update(const XCUIDemoInputState& inp
if (event.type == UIInputEventType::KeyDown &&
!event.repeat &&
summary.shortcutHandled &&
summary.commandId == kToggleAccentCommandId) {
ActivateNode(state, state.toggleButtonId);
return;
summary.shortcutHandled) {
if (BridgeCommand(state, summary.commandId)) {
return;
}
}
const UIElementId focusedElementId =

View File

@@ -300,8 +300,6 @@ private:
XCUIHostedPreviewStats m_lastStats = {};
};
std::unique_ptr<IXCUIHostedPreviewPresenter> CreateImGuiXCUIHostedPreviewPresenter();
inline std::unique_ptr<IXCUIHostedPreviewPresenter> CreateQueuedNativeXCUIHostedPreviewPresenter(
XCUIHostedPreviewQueue& queue,
XCUIHostedPreviewSurfaceRegistry& surfaceRegistry) {

View File

@@ -10,6 +10,7 @@
#include <XCEngine/UI/Types.h>
#include <XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h>
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
#include <XCEngine/UI/Widgets/UIKeyboardNavigationModel.h>
#include <XCEngine/UI/Widgets/UISelectionModel.h>
#include <algorithm>
@@ -80,6 +81,9 @@ struct RuntimeBuildContext {
std::unordered_map<std::string, std::size_t> nodeIndexById = {};
std::unordered_map<std::string, UIRect> rectsById = {};
UIWidgets::UIExpansionModel expansionModel = {};
UIWidgets::UIKeyboardNavigationModel keyboardNavigationModel = {};
std::string keyboardNavigationScopeKey = {};
bool navigationOwnsSelection = false;
UIWidgets::UISelectionModel selectionModel = {};
bool documentsReady = false;
std::string statusMessage = {};
@@ -92,6 +96,11 @@ struct RuntimeBuildContext {
fs::file_time_type themeWriteTime = {};
};
struct KeyboardNavigationScope {
std::string key = {};
std::vector<std::size_t> itemIndices = {};
};
String ToContainersString(const std::string& value) {
return String(value.c_str());
}
@@ -660,6 +669,458 @@ std::vector<std::size_t> CollectVisibleChildren(
return visibleChildren;
}
UIWidgets::UIEditorCollectionPrimitiveKind GetPrimitiveKind(
const RuntimeBuildContext& state,
std::size_t nodeIndex) {
return UIWidgets::ClassifyUIEditorCollectionPrimitive(state.nodes[nodeIndex].tagName);
}
bool IsKeyboardNavigableKind(UIWidgets::UIEditorCollectionPrimitiveKind kind) {
return kind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem ||
kind == UIWidgets::UIEditorCollectionPrimitiveKind::ListItem ||
kind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection ||
kind == UIWidgets::UIEditorCollectionPrimitiveKind::FieldRow;
}
bool HasKeyboardNavigationInput(const XCUILayoutLabInputState& input) {
return input.navigatePrevious ||
input.navigateNext ||
input.navigateHome ||
input.navigateEnd ||
input.navigateCollapse ||
input.navigateExpand;
}
std::size_t FindNodeIndexById(
const RuntimeBuildContext& state,
const std::string& elementId) {
if (elementId.empty()) {
return kInvalidIndex;
}
const auto it = state.nodeIndexById.find(elementId);
return it != state.nodeIndexById.end() ? it->second : kInvalidIndex;
}
std::size_t FindAncestorByKind(
const RuntimeBuildContext& state,
std::size_t nodeIndex,
UIWidgets::UIEditorCollectionPrimitiveKind kind) {
std::size_t ancestorIndex = nodeIndex;
while (ancestorIndex != kInvalidIndex) {
if (GetPrimitiveKind(state, ancestorIndex) == kind) {
return ancestorIndex;
}
ancestorIndex = state.nodes[ancestorIndex].parentIndex;
}
return kInvalidIndex;
}
std::vector<std::size_t> CollectVisibleChildrenOfKind(
const RuntimeBuildContext& state,
std::size_t nodeIndex,
UIWidgets::UIEditorCollectionPrimitiveKind kind) {
std::vector<std::size_t> itemIndices = {};
if (nodeIndex == kInvalidIndex) {
return itemIndices;
}
const LayoutNode& node = state.nodes[nodeIndex];
itemIndices.reserve(node.children.size());
for (const std::size_t childIndex : node.children) {
if (GetPrimitiveKind(state, childIndex) != kind ||
!IsNodeVisible(state, childIndex)) {
continue;
}
itemIndices.push_back(childIndex);
}
return itemIndices;
}
std::size_t FindItemOffset(
const std::vector<std::size_t>& itemIndices,
std::size_t nodeIndex) {
for (std::size_t itemOffset = 0; itemOffset < itemIndices.size(); ++itemOffset) {
if (itemIndices[itemOffset] == nodeIndex) {
return itemOffset;
}
}
return kInvalidIndex;
}
KeyboardNavigationScope BuildKeyboardNavigationScopeForNode(
const RuntimeBuildContext& state,
std::size_t nodeIndex) {
KeyboardNavigationScope scope = {};
if (nodeIndex == kInvalidIndex) {
return scope;
}
const UIWidgets::UIEditorCollectionPrimitiveKind kind = GetPrimitiveKind(state, nodeIndex);
if (kind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem) {
const std::size_t treeViewIndex = FindAncestorByKind(
state,
state.nodes[nodeIndex].parentIndex,
UIWidgets::UIEditorCollectionPrimitiveKind::TreeView);
if (treeViewIndex == kInvalidIndex) {
return scope;
}
scope.key = "tree:" + state.nodes[treeViewIndex].id;
scope.itemIndices = CollectVisibleChildrenOfKind(
state,
treeViewIndex,
UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem);
return scope;
}
if (kind == UIWidgets::UIEditorCollectionPrimitiveKind::ListItem) {
const std::size_t listViewIndex = FindAncestorByKind(
state,
state.nodes[nodeIndex].parentIndex,
UIWidgets::UIEditorCollectionPrimitiveKind::ListView);
if (listViewIndex == kInvalidIndex) {
return scope;
}
scope.key = "list:" + state.nodes[listViewIndex].id;
scope.itemIndices = CollectVisibleChildrenOfKind(
state,
listViewIndex,
UIWidgets::UIEditorCollectionPrimitiveKind::ListItem);
return scope;
}
if (kind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection) {
const std::size_t propertyGroupIndex = state.nodes[nodeIndex].parentIndex;
if (propertyGroupIndex == kInvalidIndex) {
return scope;
}
scope.key = "property-sections:" + state.nodes[propertyGroupIndex].id;
scope.itemIndices = CollectVisibleChildrenOfKind(
state,
propertyGroupIndex,
UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection);
return scope;
}
if (kind == UIWidgets::UIEditorCollectionPrimitiveKind::FieldRow) {
const std::size_t propertySectionIndex = FindAncestorByKind(
state,
state.nodes[nodeIndex].parentIndex,
UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection);
if (propertySectionIndex == kInvalidIndex) {
return scope;
}
scope.key = "property-fields:" + state.nodes[propertySectionIndex].id;
scope.itemIndices.push_back(propertySectionIndex);
std::vector<std::size_t> fieldRows = CollectVisibleChildrenOfKind(
state,
propertySectionIndex,
UIWidgets::UIEditorCollectionPrimitiveKind::FieldRow);
scope.itemIndices.insert(
scope.itemIndices.end(),
fieldRows.begin(),
fieldRows.end());
}
return scope;
}
std::size_t FindFirstVisibleKeyboardNavigableNode(const RuntimeBuildContext& state) {
for (std::size_t nodeIndex = 0; nodeIndex < state.nodes.size(); ++nodeIndex) {
if (!IsNodeVisible(state, nodeIndex) ||
!IsKeyboardNavigableKind(GetPrimitiveKind(state, nodeIndex))) {
continue;
}
return nodeIndex;
}
return kInvalidIndex;
}
void ClearKeyboardNavigationState(RuntimeBuildContext& state) {
state.keyboardNavigationModel = UIWidgets::UIKeyboardNavigationModel();
state.keyboardNavigationScopeKey.clear();
}
KeyboardNavigationScope ResolveKeyboardNavigationScope(
const RuntimeBuildContext& state,
std::size_t hoveredIndex,
bool allowFallback) {
const std::size_t selectedIndex = FindNodeIndexById(
state,
state.selectionModel.GetSelectedId());
if (selectedIndex != kInvalidIndex &&
IsKeyboardNavigableKind(GetPrimitiveKind(state, selectedIndex))) {
return BuildKeyboardNavigationScopeForNode(state, selectedIndex);
}
if (hoveredIndex != kInvalidIndex &&
IsKeyboardNavigableKind(GetPrimitiveKind(state, hoveredIndex))) {
return BuildKeyboardNavigationScopeForNode(state, hoveredIndex);
}
if (allowFallback && !state.selectionModel.HasSelection()) {
return BuildKeyboardNavigationScopeForNode(
state,
FindFirstVisibleKeyboardNavigableNode(state));
}
return KeyboardNavigationScope();
}
bool ApplyKeyboardNavigationSelection(
RuntimeBuildContext& state,
const KeyboardNavigationScope& scope) {
if (!state.keyboardNavigationModel.HasCurrentIndex()) {
return false;
}
const std::size_t currentIndex = state.keyboardNavigationModel.GetCurrentIndex();
if (currentIndex >= scope.itemIndices.size()) {
return false;
}
state.selectionModel.SetSelection(state.nodes[scope.itemIndices[currentIndex]].id);
state.navigationOwnsSelection = true;
return true;
}
void SyncKeyboardNavigationScope(
RuntimeBuildContext& state,
const KeyboardNavigationScope& scope) {
if (scope.key.empty()) {
if (!state.navigationOwnsSelection) {
ClearKeyboardNavigationState(state);
}
return;
}
if (state.keyboardNavigationScopeKey != scope.key) {
ClearKeyboardNavigationState(state);
state.keyboardNavigationScopeKey = scope.key;
}
state.keyboardNavigationModel.SetItemCount(scope.itemIndices.size());
if (scope.itemIndices.empty()) {
if (state.navigationOwnsSelection) {
state.selectionModel.ClearSelection();
state.navigationOwnsSelection = false;
}
return;
}
const std::size_t selectedIndex = FindNodeIndexById(
state,
state.selectionModel.GetSelectedId());
const std::size_t selectedOffset = FindItemOffset(scope.itemIndices, selectedIndex);
if (selectedOffset != kInvalidIndex) {
state.keyboardNavigationModel.SetCurrentIndex(selectedOffset);
return;
}
if (state.navigationOwnsSelection &&
state.keyboardNavigationModel.HasCurrentIndex()) {
ApplyKeyboardNavigationSelection(state, scope);
}
}
std::size_t FindTreeParentItemIndex(
const RuntimeBuildContext& state,
std::size_t nodeIndex) {
if (nodeIndex == kInvalidIndex ||
GetPrimitiveKind(state, nodeIndex) != UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem) {
return kInvalidIndex;
}
const std::size_t treeViewIndex = FindAncestorByKind(
state,
state.nodes[nodeIndex].parentIndex,
UIWidgets::UIEditorCollectionPrimitiveKind::TreeView);
if (treeViewIndex == kInvalidIndex) {
return kInvalidIndex;
}
const float indentLevel = ResolveTreeIndentLevel(state.nodes[nodeIndex]);
if (indentLevel <= 0.0f) {
return kInvalidIndex;
}
const LayoutNode& treeView = state.nodes[treeViewIndex];
const auto siblingIt = std::find(treeView.children.begin(), treeView.children.end(), nodeIndex);
if (siblingIt == treeView.children.end()) {
return kInvalidIndex;
}
for (auto it = siblingIt; it != treeView.children.begin();) {
--it;
if (ResolveTreeIndentLevel(state.nodes[*it]) < indentLevel) {
return *it;
}
}
return kInvalidIndex;
}
std::size_t FindFirstTreeChildItemIndex(
const RuntimeBuildContext& state,
std::size_t nodeIndex) {
if (!HasTreeItemChildren(state, nodeIndex) ||
state.nodes[nodeIndex].parentIndex == kInvalidIndex) {
return kInvalidIndex;
}
const LayoutNode& node = state.nodes[nodeIndex];
const LayoutNode& treeView = state.nodes[node.parentIndex];
const float indentLevel = ResolveTreeIndentLevel(node);
const auto siblingIt = std::find(treeView.children.begin(), treeView.children.end(), nodeIndex);
if (siblingIt == treeView.children.end()) {
return kInvalidIndex;
}
for (auto it = siblingIt + 1; it != treeView.children.end(); ++it) {
const std::size_t candidateIndex = *it;
const float candidateIndent = ResolveTreeIndentLevel(state.nodes[candidateIndex]);
if (candidateIndent <= indentLevel) {
break;
}
if (IsNodeVisible(state, candidateIndex)) {
return candidateIndex;
}
}
return kInvalidIndex;
}
bool HandleKeyboardExpand(RuntimeBuildContext& state) {
const std::size_t selectedIndex = FindNodeIndexById(
state,
state.selectionModel.GetSelectedId());
if (selectedIndex == kInvalidIndex) {
return false;
}
const UIWidgets::UIEditorCollectionPrimitiveKind kind = GetPrimitiveKind(state, selectedIndex);
if (kind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem) {
if (!HasTreeItemChildren(state, selectedIndex)) {
return false;
}
if (!IsNodeExpanded(state, selectedIndex)) {
state.expansionModel.Expand(state.nodes[selectedIndex].id);
state.navigationOwnsSelection = true;
return true;
}
const std::size_t childIndex = FindFirstTreeChildItemIndex(state, selectedIndex);
if (childIndex == kInvalidIndex) {
return false;
}
state.selectionModel.SetSelection(state.nodes[childIndex].id);
state.navigationOwnsSelection = true;
return true;
}
if (kind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection) {
if (!IsNodeExpanded(state, selectedIndex)) {
state.expansionModel.Expand(state.nodes[selectedIndex].id);
state.navigationOwnsSelection = true;
return true;
}
const std::vector<std::size_t> fieldRows = CollectVisibleChildrenOfKind(
state,
selectedIndex,
UIWidgets::UIEditorCollectionPrimitiveKind::FieldRow);
if (fieldRows.empty()) {
return false;
}
state.selectionModel.SetSelection(state.nodes[fieldRows.front()].id);
state.navigationOwnsSelection = true;
return true;
}
return false;
}
bool HandleKeyboardCollapse(RuntimeBuildContext& state) {
const std::size_t selectedIndex = FindNodeIndexById(
state,
state.selectionModel.GetSelectedId());
if (selectedIndex == kInvalidIndex) {
return false;
}
const UIWidgets::UIEditorCollectionPrimitiveKind kind = GetPrimitiveKind(state, selectedIndex);
if (kind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem) {
if (HasTreeItemChildren(state, selectedIndex) &&
IsNodeExpanded(state, selectedIndex)) {
state.expansionModel.Collapse(state.nodes[selectedIndex].id);
state.navigationOwnsSelection = true;
return true;
}
const std::size_t parentIndex = FindTreeParentItemIndex(state, selectedIndex);
if (parentIndex == kInvalidIndex) {
return false;
}
state.selectionModel.SetSelection(state.nodes[parentIndex].id);
state.navigationOwnsSelection = true;
return true;
}
if (kind == UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection) {
if (!IsNodeExpanded(state, selectedIndex)) {
return false;
}
state.expansionModel.Collapse(state.nodes[selectedIndex].id);
state.navigationOwnsSelection = true;
return true;
}
if (kind == UIWidgets::UIEditorCollectionPrimitiveKind::FieldRow) {
const std::size_t propertySectionIndex = FindAncestorByKind(
state,
state.nodes[selectedIndex].parentIndex,
UIWidgets::UIEditorCollectionPrimitiveKind::PropertySection);
if (propertySectionIndex == kInvalidIndex) {
return false;
}
state.selectionModel.SetSelection(state.nodes[propertySectionIndex].id);
state.navigationOwnsSelection = true;
return true;
}
return false;
}
bool MoveKeyboardNavigationSelection(
RuntimeBuildContext& state,
const KeyboardNavigationScope& scope,
bool (UIWidgets::UIKeyboardNavigationModel::*moveFn)()) {
if (scope.itemIndices.empty() ||
!(state.keyboardNavigationModel.*moveFn)()) {
return false;
}
return ApplyKeyboardNavigationSelection(state, scope);
}
void SeedDefaultExpansionState(RuntimeBuildContext& state) {
state.expansionModel.Clear();
for (std::size_t nodeIndex = 0; nodeIndex < state.nodes.size(); ++nodeIndex) {
@@ -1197,6 +1658,8 @@ bool XCUILayoutLabRuntime::ReloadDocuments() {
state.nodeIndexById.clear();
state.rectsById.clear();
state.expansionModel.Clear();
ClearKeyboardNavigationState(state);
state.navigationOwnsSelection = false;
state.selectionModel.ClearSelection();
state.documentSource.SetPathSet(XCUIAssetDocumentSource::MakeLayoutLabPathSet());
@@ -1285,8 +1748,55 @@ const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::Update(const XCUILayoutLab
state.expansionModel.ToggleExpanded(state.nodes[hoveredIndex].id);
}
state.selectionModel.SetSelection(state.nodes[hoveredIndex].id);
state.navigationOwnsSelection =
IsKeyboardNavigableKind(GetPrimitiveKind(state, hoveredIndex));
} else {
state.selectionModel.ClearSelection();
state.navigationOwnsSelection = false;
}
}
KeyboardNavigationScope navigationScope = ResolveKeyboardNavigationScope(
state,
hoveredIndex,
HasKeyboardNavigationInput(input));
SyncKeyboardNavigationScope(state, navigationScope);
if (HasKeyboardNavigationInput(input) &&
!navigationScope.itemIndices.empty()) {
if (input.navigateCollapse) {
HandleKeyboardCollapse(state);
}
if (input.navigateExpand) {
HandleKeyboardExpand(state);
}
navigationScope = ResolveKeyboardNavigationScope(state, hoveredIndex, true);
SyncKeyboardNavigationScope(state, navigationScope);
if (input.navigateHome) {
MoveKeyboardNavigationSelection(
state,
navigationScope,
&UIWidgets::UIKeyboardNavigationModel::MoveHome);
}
if (input.navigateEnd) {
MoveKeyboardNavigationSelection(
state,
navigationScope,
&UIWidgets::UIKeyboardNavigationModel::MoveEnd);
}
if (input.navigatePrevious) {
MoveKeyboardNavigationSelection(
state,
navigationScope,
&UIWidgets::UIKeyboardNavigationModel::MovePrevious);
}
if (input.navigateNext) {
MoveKeyboardNavigationSelection(
state,
navigationScope,
&UIWidgets::UIKeyboardNavigationModel::MoveNext);
}
}

View File

@@ -18,6 +18,12 @@ struct XCUILayoutLabInputState {
UI::UIPoint pointerPosition = {};
bool pointerInside = false;
bool pointerPressed = false;
bool navigatePrevious = false;
bool navigateNext = false;
bool navigateHome = false;
bool navigateEnd = false;
bool navigateCollapse = false;
bool navigateExpand = false;
};
struct XCUILayoutLabFrameStats {

View File

@@ -4,6 +4,7 @@
#include <XCEngine/UI/DrawData.h>
#include <cstdint>
#include <memory>
#include <string_view>
@@ -11,6 +12,17 @@ namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
enum class XCUIPanelCanvasHostBackend : std::uint8_t {
Null = 0,
ImGui
};
struct XCUIPanelCanvasHostCapabilities {
bool supportsPointerHitTesting = false;
bool supportsHostedSurfaceImages = false;
bool supportsPrimitiveOverlays = false;
};
struct XCUIPanelCanvasRequest {
const char* childId = nullptr;
float height = 0.0f;
@@ -38,6 +50,9 @@ class IXCUIPanelCanvasHost {
public:
virtual ~IXCUIPanelCanvasHost() = default;
virtual const char* GetDebugName() const = 0;
virtual XCUIPanelCanvasHostBackend GetBackend() const = 0;
virtual XCUIPanelCanvasHostCapabilities GetCapabilities() const = 0;
virtual XCUIPanelCanvasSession BeginCanvas(const XCUIPanelCanvasRequest& request) = 0;
virtual void DrawFilledRect(
const ::XCEngine::UI::UIRect& rect,
@@ -56,7 +71,7 @@ public:
virtual void EndCanvas() = 0;
};
std::unique_ptr<IXCUIPanelCanvasHost> CreateImGuiXCUIPanelCanvasHost();
std::unique_ptr<IXCUIPanelCanvasHost> CreateNullXCUIPanelCanvasHost();
} // namespace XCUIBackend
} // namespace Editor

View File

@@ -0,0 +1,295 @@
#include "XCUIBackend/XCUIShellChromeState.h"
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
namespace {
constexpr std::size_t ToIndex(XCUIShellPanelId panelId) {
return static_cast<std::size_t>(panelId);
}
} // namespace
XCUIShellChromeState::XCUIShellChromeState() {
m_panels[ToIndex(XCUIShellPanelId::XCUIDemo)] = {
XCUIShellPanelId::XCUIDemo,
"XCUI Demo",
"XCUI Demo",
"new_editor.panels.xcui_demo",
true,
true,
XCUIShellHostedPreviewMode::NativeOffscreen
};
m_panels[ToIndex(XCUIShellPanelId::XCUILayoutLab)] = {
XCUIShellPanelId::XCUILayoutLab,
"XCUI Layout Lab",
"XCUI Layout Lab",
"new_editor.panels.xcui_layout_lab",
true,
true,
XCUIShellHostedPreviewMode::LegacyImGui
};
}
const XCUIShellViewToggleState& XCUIShellChromeState::GetViewToggles() const {
return m_viewToggles;
}
const std::array<XCUIShellPanelChromeState, static_cast<std::size_t>(XCUIShellPanelId::Count)>&
XCUIShellChromeState::GetPanels() const {
return m_panels;
}
const XCUIShellPanelChromeState* XCUIShellChromeState::TryGetPanelState(XCUIShellPanelId panelId) const {
const std::size_t index = ToIndex(panelId);
if (index >= m_panels.size()) {
return nullptr;
}
return &m_panels[index];
}
XCUIShellPanelChromeState* XCUIShellChromeState::TryGetPanelStateMutable(XCUIShellPanelId panelId) {
const std::size_t index = ToIndex(panelId);
if (index >= m_panels.size()) {
return nullptr;
}
return &m_panels[index];
}
bool XCUIShellChromeState::IsPanelVisible(XCUIShellPanelId panelId) const {
const XCUIShellPanelChromeState* panelState = TryGetPanelState(panelId);
return panelState != nullptr && panelState->visible;
}
bool XCUIShellChromeState::SetPanelVisible(XCUIShellPanelId panelId, bool visible) {
XCUIShellPanelChromeState* panelState = TryGetPanelStateMutable(panelId);
if (panelState == nullptr || panelState->visible == visible) {
return false;
}
panelState->visible = visible;
return true;
}
bool XCUIShellChromeState::TogglePanelVisible(XCUIShellPanelId panelId) {
XCUIShellPanelChromeState* panelState = TryGetPanelStateMutable(panelId);
if (panelState == nullptr) {
return false;
}
panelState->visible = !panelState->visible;
return true;
}
bool XCUIShellChromeState::IsHostedPreviewEnabled(XCUIShellPanelId panelId) const {
const XCUIShellPanelChromeState* panelState = TryGetPanelState(panelId);
return panelState != nullptr && panelState->hostedPreviewEnabled;
}
bool XCUIShellChromeState::SetHostedPreviewEnabled(XCUIShellPanelId panelId, bool enabled) {
XCUIShellPanelChromeState* panelState = TryGetPanelStateMutable(panelId);
if (panelState == nullptr || panelState->hostedPreviewEnabled == enabled) {
return false;
}
panelState->hostedPreviewEnabled = enabled;
return true;
}
XCUIShellHostedPreviewMode XCUIShellChromeState::GetHostedPreviewMode(XCUIShellPanelId panelId) const {
const XCUIShellPanelChromeState* panelState = TryGetPanelState(panelId);
return panelState != nullptr
? panelState->previewMode
: XCUIShellHostedPreviewMode::LegacyImGui;
}
XCUIShellHostedPreviewState XCUIShellChromeState::GetHostedPreviewState(XCUIShellPanelId panelId) const {
const XCUIShellPanelChromeState* panelState = TryGetPanelState(panelId);
if (panelState == nullptr || !panelState->hostedPreviewEnabled) {
return XCUIShellHostedPreviewState::Disabled;
}
return panelState->previewMode == XCUIShellHostedPreviewMode::NativeOffscreen
? XCUIShellHostedPreviewState::NativeOffscreen
: XCUIShellHostedPreviewState::LegacyImGui;
}
bool XCUIShellChromeState::IsNativeHostedPreviewActive(XCUIShellPanelId panelId) const {
return GetHostedPreviewState(panelId) == XCUIShellHostedPreviewState::NativeOffscreen;
}
bool XCUIShellChromeState::IsLegacyHostedPreviewActive(XCUIShellPanelId panelId) const {
return GetHostedPreviewState(panelId) == XCUIShellHostedPreviewState::LegacyImGui;
}
bool XCUIShellChromeState::SetHostedPreviewMode(
XCUIShellPanelId panelId,
XCUIShellHostedPreviewMode mode) {
XCUIShellPanelChromeState* panelState = TryGetPanelStateMutable(panelId);
if (panelState == nullptr || panelState->previewMode == mode) {
return false;
}
panelState->previewMode = mode;
return true;
}
bool XCUIShellChromeState::ToggleHostedPreviewMode(XCUIShellPanelId panelId) {
XCUIShellPanelChromeState* panelState = TryGetPanelStateMutable(panelId);
if (panelState == nullptr) {
return false;
}
panelState->previewMode =
panelState->previewMode == XCUIShellHostedPreviewMode::NativeOffscreen
? XCUIShellHostedPreviewMode::LegacyImGui
: XCUIShellHostedPreviewMode::NativeOffscreen;
return true;
}
bool XCUIShellChromeState::GetViewToggle(XCUIShellViewToggleId toggleId) const {
switch (toggleId) {
case XCUIShellViewToggleId::ImGuiDemoWindow:
return m_viewToggles.imguiDemoWindowVisible;
case XCUIShellViewToggleId::NativeBackdrop:
return m_viewToggles.nativeBackdropVisible;
case XCUIShellViewToggleId::PulseAccent:
return m_viewToggles.pulseAccentEnabled;
case XCUIShellViewToggleId::NativeXCUIOverlay:
return m_viewToggles.nativeXCUIOverlayVisible;
case XCUIShellViewToggleId::HostedPreviewHud:
return m_viewToggles.hostedPreviewHudVisible;
case XCUIShellViewToggleId::Count:
default:
return false;
}
}
bool XCUIShellChromeState::SetViewToggle(XCUIShellViewToggleId toggleId, bool enabled) {
bool* target = nullptr;
switch (toggleId) {
case XCUIShellViewToggleId::ImGuiDemoWindow:
target = &m_viewToggles.imguiDemoWindowVisible;
break;
case XCUIShellViewToggleId::NativeBackdrop:
target = &m_viewToggles.nativeBackdropVisible;
break;
case XCUIShellViewToggleId::PulseAccent:
target = &m_viewToggles.pulseAccentEnabled;
break;
case XCUIShellViewToggleId::NativeXCUIOverlay:
target = &m_viewToggles.nativeXCUIOverlayVisible;
break;
case XCUIShellViewToggleId::HostedPreviewHud:
target = &m_viewToggles.hostedPreviewHudVisible;
break;
case XCUIShellViewToggleId::Count:
default:
return false;
}
if (*target == enabled) {
return false;
}
*target = enabled;
return true;
}
bool XCUIShellChromeState::ToggleViewToggle(XCUIShellViewToggleId toggleId) {
return SetViewToggle(toggleId, !GetViewToggle(toggleId));
}
bool XCUIShellChromeState::HasCommand(std::string_view commandId) const {
return commandId == GetPanelVisibilityCommandId(XCUIShellPanelId::XCUIDemo) ||
commandId == GetPanelVisibilityCommandId(XCUIShellPanelId::XCUILayoutLab) ||
commandId == GetViewToggleCommandId(XCUIShellViewToggleId::ImGuiDemoWindow) ||
commandId == GetViewToggleCommandId(XCUIShellViewToggleId::NativeBackdrop) ||
commandId == GetViewToggleCommandId(XCUIShellViewToggleId::PulseAccent) ||
commandId == GetViewToggleCommandId(XCUIShellViewToggleId::NativeXCUIOverlay) ||
commandId == GetViewToggleCommandId(XCUIShellViewToggleId::HostedPreviewHud) ||
commandId == GetPanelPreviewModeCommandId(XCUIShellPanelId::XCUIDemo) ||
commandId == GetPanelPreviewModeCommandId(XCUIShellPanelId::XCUILayoutLab);
}
bool XCUIShellChromeState::InvokeCommand(std::string_view commandId) {
if (commandId == XCUIShellChromeCommandIds::ToggleXCUIDemoPanel) {
return TogglePanelVisible(XCUIShellPanelId::XCUIDemo);
}
if (commandId == XCUIShellChromeCommandIds::ToggleXCUILayoutLabPanel) {
return TogglePanelVisible(XCUIShellPanelId::XCUILayoutLab);
}
if (commandId == XCUIShellChromeCommandIds::ToggleImGuiDemoWindow) {
return ToggleViewToggle(XCUIShellViewToggleId::ImGuiDemoWindow);
}
if (commandId == XCUIShellChromeCommandIds::ToggleNativeBackdrop) {
return ToggleViewToggle(XCUIShellViewToggleId::NativeBackdrop);
}
if (commandId == XCUIShellChromeCommandIds::TogglePulseAccent) {
return ToggleViewToggle(XCUIShellViewToggleId::PulseAccent);
}
if (commandId == XCUIShellChromeCommandIds::ToggleNativeXCUIOverlay) {
return ToggleViewToggle(XCUIShellViewToggleId::NativeXCUIOverlay);
}
if (commandId == XCUIShellChromeCommandIds::ToggleHostedPreviewHud) {
return ToggleViewToggle(XCUIShellViewToggleId::HostedPreviewHud);
}
if (commandId == XCUIShellChromeCommandIds::ToggleNativeDemoPanelPreview) {
return ToggleHostedPreviewMode(XCUIShellPanelId::XCUIDemo);
}
if (commandId == XCUIShellChromeCommandIds::ToggleNativeLayoutLabPreview) {
return ToggleHostedPreviewMode(XCUIShellPanelId::XCUILayoutLab);
}
return false;
}
std::string_view XCUIShellChromeState::GetPanelVisibilityCommandId(XCUIShellPanelId panelId) {
switch (panelId) {
case XCUIShellPanelId::XCUIDemo:
return XCUIShellChromeCommandIds::ToggleXCUIDemoPanel;
case XCUIShellPanelId::XCUILayoutLab:
return XCUIShellChromeCommandIds::ToggleXCUILayoutLabPanel;
case XCUIShellPanelId::Count:
default:
return {};
}
}
std::string_view XCUIShellChromeState::GetPanelPreviewModeCommandId(XCUIShellPanelId panelId) {
switch (panelId) {
case XCUIShellPanelId::XCUIDemo:
return XCUIShellChromeCommandIds::ToggleNativeDemoPanelPreview;
case XCUIShellPanelId::XCUILayoutLab:
return XCUIShellChromeCommandIds::ToggleNativeLayoutLabPreview;
case XCUIShellPanelId::Count:
default:
return {};
}
}
std::string_view XCUIShellChromeState::GetViewToggleCommandId(XCUIShellViewToggleId toggleId) {
switch (toggleId) {
case XCUIShellViewToggleId::ImGuiDemoWindow:
return XCUIShellChromeCommandIds::ToggleImGuiDemoWindow;
case XCUIShellViewToggleId::NativeBackdrop:
return XCUIShellChromeCommandIds::ToggleNativeBackdrop;
case XCUIShellViewToggleId::PulseAccent:
return XCUIShellChromeCommandIds::TogglePulseAccent;
case XCUIShellViewToggleId::NativeXCUIOverlay:
return XCUIShellChromeCommandIds::ToggleNativeXCUIOverlay;
case XCUIShellViewToggleId::HostedPreviewHud:
return XCUIShellChromeCommandIds::ToggleHostedPreviewHud;
case XCUIShellViewToggleId::Count:
default:
return {};
}
}
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,109 @@
#pragma once
#include <array>
#include <cstdint>
#include <string_view>
namespace XCEngine {
namespace Editor {
namespace XCUIBackend {
enum class XCUIShellPanelId : std::uint8_t {
XCUIDemo = 0,
XCUILayoutLab,
Count
};
enum class XCUIShellViewToggleId : std::uint8_t {
ImGuiDemoWindow = 0,
NativeBackdrop,
PulseAccent,
NativeXCUIOverlay,
HostedPreviewHud,
Count
};
enum class XCUIShellHostedPreviewMode : std::uint8_t {
LegacyImGui = 0,
NativeOffscreen
};
enum class XCUIShellHostedPreviewState : std::uint8_t {
Disabled = 0,
LegacyImGui,
NativeOffscreen
};
struct XCUIShellPanelChromeState {
XCUIShellPanelId panelId = XCUIShellPanelId::XCUIDemo;
std::string_view panelTitle = {};
std::string_view previewDebugName = {};
std::string_view previewDebugSource = {};
bool visible = true;
bool hostedPreviewEnabled = true;
XCUIShellHostedPreviewMode previewMode = XCUIShellHostedPreviewMode::LegacyImGui;
};
struct XCUIShellViewToggleState {
bool imguiDemoWindowVisible = false;
bool nativeBackdropVisible = true;
bool pulseAccentEnabled = true;
bool nativeXCUIOverlayVisible = true;
bool hostedPreviewHudVisible = true;
};
struct XCUIShellChromeCommandIds {
static constexpr const char* ToggleXCUIDemoPanel = "new_editor.view.xcui_demo";
static constexpr const char* ToggleXCUILayoutLabPanel = "new_editor.view.xcui_layout_lab";
static constexpr const char* ToggleImGuiDemoWindow = "new_editor.view.imgui_demo";
static constexpr const char* ToggleNativeBackdrop = "new_editor.view.native_backdrop";
static constexpr const char* TogglePulseAccent = "new_editor.view.pulse_accent";
static constexpr const char* ToggleNativeXCUIOverlay = "new_editor.view.native_xcui_overlay";
static constexpr const char* ToggleHostedPreviewHud = "new_editor.view.hosted_preview_hud";
static constexpr const char* ToggleNativeDemoPanelPreview = "new_editor.view.native_demo_panel_preview";
static constexpr const char* ToggleNativeLayoutLabPreview = "new_editor.view.native_layout_lab_preview";
};
class XCUIShellChromeState {
public:
XCUIShellChromeState();
const XCUIShellViewToggleState& GetViewToggles() const;
const std::array<XCUIShellPanelChromeState, static_cast<std::size_t>(XCUIShellPanelId::Count)>& GetPanels() const;
const XCUIShellPanelChromeState* TryGetPanelState(XCUIShellPanelId panelId) const;
bool IsPanelVisible(XCUIShellPanelId panelId) const;
bool SetPanelVisible(XCUIShellPanelId panelId, bool visible);
bool TogglePanelVisible(XCUIShellPanelId panelId);
bool IsHostedPreviewEnabled(XCUIShellPanelId panelId) const;
bool SetHostedPreviewEnabled(XCUIShellPanelId panelId, bool enabled);
XCUIShellHostedPreviewMode GetHostedPreviewMode(XCUIShellPanelId panelId) const;
XCUIShellHostedPreviewState GetHostedPreviewState(XCUIShellPanelId panelId) const;
bool IsNativeHostedPreviewActive(XCUIShellPanelId panelId) const;
bool IsLegacyHostedPreviewActive(XCUIShellPanelId panelId) const;
bool SetHostedPreviewMode(XCUIShellPanelId panelId, XCUIShellHostedPreviewMode mode);
bool ToggleHostedPreviewMode(XCUIShellPanelId panelId);
bool GetViewToggle(XCUIShellViewToggleId toggleId) const;
bool SetViewToggle(XCUIShellViewToggleId toggleId, bool enabled);
bool ToggleViewToggle(XCUIShellViewToggleId toggleId);
bool HasCommand(std::string_view commandId) const;
bool InvokeCommand(std::string_view commandId);
static std::string_view GetPanelVisibilityCommandId(XCUIShellPanelId panelId);
static std::string_view GetPanelPreviewModeCommandId(XCUIShellPanelId panelId);
static std::string_view GetViewToggleCommandId(XCUIShellViewToggleId toggleId);
private:
XCUIShellPanelChromeState* TryGetPanelStateMutable(XCUIShellPanelId panelId);
XCUIShellViewToggleState m_viewToggles = {};
std::array<XCUIShellPanelChromeState, static_cast<std::size_t>(XCUIShellPanelId::Count)> m_panels = {};
};
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -2,6 +2,7 @@
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
#include <XCEngine/UI/Runtime/UISceneRuntimeContext.h>
#include <XCEngine/UI/Runtime/UIScreenStackController.h>
#include <XCEngine/UI/Runtime/UISystem.h>
@@ -9,12 +10,14 @@
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
namespace {
using XCEngine::UI::Runtime::UIScreenAsset;
using XCEngine::UI::Runtime::UIScreenFrameInput;
using XCEngine::UI::Runtime::UIScreenPlayer;
using XCEngine::UI::Runtime::UISceneRuntimeContext;
using XCEngine::UI::Runtime::UIDocumentScreenHost;
using XCEngine::UI::Runtime::UIScreenStackController;
using XCEngine::UI::Runtime::UISystem;
@@ -93,6 +96,69 @@ UIScreenFrameInput BuildInputState(std::uint64_t frameIndex = 1u) {
return input;
}
const XCEngine::UI::Runtime::UISystemPresentedLayer* FindPresentedLayerById(
const XCEngine::UI::Runtime::UISystemFrameResult& frame,
XCEngine::UI::Runtime::UIScreenLayerId layerId) {
for (const XCEngine::UI::Runtime::UISystemPresentedLayer& layer : frame.layers) {
if (layer.layerId == layerId) {
return &layer;
}
}
return nullptr;
}
class RecordingDocumentHost final : public XCEngine::UI::Runtime::IUIScreenDocumentHost {
public:
struct BuildCall {
std::string displayName = {};
UIScreenFrameInput input = {};
};
XCEngine::UI::Runtime::UIScreenLoadResult LoadScreen(const UIScreenAsset& asset) override {
XCEngine::UI::Runtime::UIScreenLoadResult result = {};
result.succeeded = asset.IsValid();
result.document.sourcePath = asset.documentPath;
result.document.displayName = asset.screenId.empty() ? asset.documentPath : asset.screenId;
return result;
}
XCEngine::UI::Runtime::UIScreenFrameResult BuildFrame(
const XCEngine::UI::Runtime::UIScreenDocument& document,
const UIScreenFrameInput& input) override {
m_buildCalls.push_back(BuildCall{ document.displayName, input });
XCEngine::UI::Runtime::UIScreenFrameResult result = {};
result.stats.documentLoaded = true;
result.stats.inputEventCount = input.events.size();
result.stats.presentedFrameIndex = input.frameIndex;
XCEngine::UI::UIDrawList& drawList = result.drawData.EmplaceDrawList(document.displayName);
drawList.AddText(
XCEngine::UI::UIPoint(input.viewportRect.x, input.viewportRect.y),
document.displayName);
result.stats.drawListCount = result.drawData.GetDrawListCount();
result.stats.commandCount = result.drawData.GetTotalCommandCount();
return result;
}
const BuildCall* FindBuildCall(const std::string& displayName) const {
for (const BuildCall& call : m_buildCalls) {
if (call.displayName == displayName) {
return &call;
}
}
return nullptr;
}
std::size_t GetBuildCallCount() const {
return m_buildCalls.size();
}
private:
std::vector<BuildCall> m_buildCalls = {};
};
} // namespace
TEST(UIRuntimeTest, ScreenPlayerBuildsDrawDataFromDocumentTree) {
@@ -115,6 +181,52 @@ TEST(UIRuntimeTest, ScreenPlayerBuildsDrawDataFromDocumentTree) {
EXPECT_EQ(player.GetPresentedFrameCount(), 1u);
}
TEST(UIRuntimeTest, ScreenPlayerConsumeLastFrameReturnsDetachedPacketAndClearsBorrowedState) {
TempFileScope viewFile("xcui_runtime_consume_player", ".xcui", BuildViewMarkup("Runtime Consume"));
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.consume.player")));
const auto& firstFrame = player.Update(BuildInputState(2u));
ASSERT_TRUE(firstFrame.stats.documentLoaded);
EXPECT_EQ(firstFrame.stats.presentedFrameIndex, 2u);
EXPECT_TRUE(DrawDataContainsText(firstFrame.drawData, "Runtime Consume"));
XCEngine::UI::Runtime::UIScreenFrameResult consumedFrame = player.ConsumeLastFrame();
EXPECT_TRUE(consumedFrame.stats.documentLoaded);
EXPECT_EQ(consumedFrame.stats.presentedFrameIndex, 2u);
EXPECT_EQ(consumedFrame.stats.drawListCount, consumedFrame.drawData.GetDrawListCount());
EXPECT_EQ(consumedFrame.stats.commandCount, consumedFrame.drawData.GetTotalCommandCount());
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "Runtime Consume"));
EXPECT_EQ(player.GetPresentedFrameCount(), 1u);
const auto& clearedFrame = player.GetLastFrame();
EXPECT_FALSE(clearedFrame.stats.documentLoaded);
EXPECT_EQ(clearedFrame.stats.presentedFrameIndex, 0u);
EXPECT_EQ(clearedFrame.drawData.GetDrawListCount(), 0u);
EXPECT_TRUE(clearedFrame.errorMessage.empty());
const auto& secondFrame = player.Update(BuildInputState(3u));
EXPECT_TRUE(secondFrame.stats.documentLoaded);
EXPECT_EQ(secondFrame.stats.presentedFrameIndex, 3u);
EXPECT_TRUE(DrawDataContainsText(secondFrame.drawData, "Runtime Consume"));
EXPECT_EQ(player.GetPresentedFrameCount(), 2u);
EXPECT_EQ(consumedFrame.stats.presentedFrameIndex, 2u);
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "Runtime Consume"));
const XCEngine::UI::Runtime::UIScreenFrameResult emptyFrame = player.ConsumeLastFrame();
EXPECT_TRUE(emptyFrame.stats.documentLoaded);
EXPECT_EQ(emptyFrame.stats.presentedFrameIndex, 3u);
EXPECT_TRUE(DrawDataContainsText(emptyFrame.drawData, "Runtime Consume"));
const XCEngine::UI::Runtime::UIScreenFrameResult clearedAgain = player.ConsumeLastFrame();
EXPECT_FALSE(clearedAgain.stats.documentLoaded);
EXPECT_EQ(clearedAgain.stats.presentedFrameIndex, 0u);
EXPECT_EQ(clearedAgain.drawData.GetDrawListCount(), 0u);
}
TEST(UIRuntimeTest, UISystemForwardsActiveScreenToPlayer) {
TempFileScope baseView("xcui_runtime_base", ".xcui", BuildViewMarkup("Base Screen"));
TempFileScope overlayView("xcui_runtime_overlay", ".xcui", BuildViewMarkup("Overlay Screen", "Modal Dialog"));
@@ -236,3 +348,224 @@ TEST(UIRuntimeTest, ScreenStackControllerReplaceTopKeepsPreviousScreenWhenReplac
EXPECT_EQ(frame.presentedLayerCount, 1u);
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Pause Menu"));
}
TEST(UIRuntimeTest, RoutesViewportAndFocusOnlyToTopInteractiveVisibleLayer) {
RecordingDocumentHost host = {};
UISystem system(host);
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
gameplayOptions.debugName = "gameplay";
gameplayOptions.acceptsInput = true;
gameplayOptions.blocksLayersBelow = false;
XCEngine::UI::Runtime::UIScreenLayerOptions overlayOptions = {};
overlayOptions.debugName = "overlay";
overlayOptions.acceptsInput = true;
overlayOptions.blocksLayersBelow = false;
const auto gameplayLayerId = system.PushScreen(
BuildScreenAsset(fs::path("gameplay_view.xcui"), "runtime.gameplay"),
gameplayOptions);
const auto overlayLayerId = system.PushScreen(
BuildScreenAsset(fs::path("overlay_view.xcui"), "runtime.overlay"),
overlayOptions);
ASSERT_NE(gameplayLayerId, 0u);
ASSERT_NE(overlayLayerId, 0u);
UIScreenFrameInput input = BuildInputState(8u);
input.viewportRect = XCEngine::UI::UIRect(15.0f, 25.0f, 1024.0f, 576.0f);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'I';
input.events.push_back(textEvent);
const auto& frame = system.Update(input);
ASSERT_EQ(frame.presentedLayerCount, 2u);
ASSERT_EQ(frame.layers.size(), 2u);
ASSERT_EQ(host.GetBuildCallCount(), 2u);
const auto* gameplayCall = host.FindBuildCall("runtime.gameplay");
const auto* overlayCall = host.FindBuildCall("runtime.overlay");
ASSERT_NE(gameplayCall, nullptr);
ASSERT_NE(overlayCall, nullptr);
EXPECT_EQ(gameplayCall->input.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(gameplayCall->input.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(gameplayCall->input.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(gameplayCall->input.viewportRect.height, input.viewportRect.height);
EXPECT_TRUE(gameplayCall->input.events.empty());
EXPECT_FALSE(gameplayCall->input.focused);
EXPECT_EQ(gameplayCall->input.frameIndex, input.frameIndex);
EXPECT_EQ(overlayCall->input.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(overlayCall->input.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(overlayCall->input.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(overlayCall->input.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(overlayCall->input.events.size(), 1u);
EXPECT_TRUE(overlayCall->input.focused);
EXPECT_EQ(overlayCall->input.frameIndex, input.frameIndex);
const auto* gameplayLayer = FindPresentedLayerById(frame, gameplayLayerId);
const auto* overlayLayer = FindPresentedLayerById(frame, overlayLayerId);
ASSERT_NE(gameplayLayer, nullptr);
ASSERT_NE(overlayLayer, nullptr);
EXPECT_EQ(gameplayLayer->stats.inputEventCount, 0u);
EXPECT_EQ(overlayLayer->stats.inputEventCount, 1u);
}
TEST(UIRuntimeTest, HiddenTopLayerLeavesUnderlyingLayerFocusedAndInteractive) {
RecordingDocumentHost host = {};
UISystem system(host);
XCEngine::UI::Runtime::UIScreenLayerOptions visibleOptions = {};
visibleOptions.debugName = "visible";
visibleOptions.acceptsInput = true;
visibleOptions.blocksLayersBelow = false;
XCEngine::UI::Runtime::UIScreenLayerOptions hiddenOptions = {};
hiddenOptions.debugName = "hidden";
hiddenOptions.visible = false;
hiddenOptions.acceptsInput = true;
hiddenOptions.blocksLayersBelow = false;
const auto visibleLayerId = system.PushScreen(
BuildScreenAsset(fs::path("visible_view.xcui"), "runtime.visible"),
visibleOptions);
const auto hiddenLayerId = system.PushScreen(
BuildScreenAsset(fs::path("hidden_view.xcui"), "runtime.hidden"),
hiddenOptions);
ASSERT_NE(visibleLayerId, 0u);
ASSERT_NE(hiddenLayerId, 0u);
UIScreenFrameInput input = BuildInputState(9u);
input.viewportRect = XCEngine::UI::UIRect(40.0f, 60.0f, 700.0f, 420.0f);
XCEngine::UI::UIInputEvent keyEvent = {};
keyEvent.type = XCEngine::UI::UIInputEventType::KeyDown;
keyEvent.keyCode = 32;
input.events.push_back(keyEvent);
const auto& frame = system.Update(input);
ASSERT_EQ(frame.presentedLayerCount, 1u);
ASSERT_EQ(frame.skippedLayerCount, 1u);
ASSERT_EQ(frame.layers.size(), 1u);
ASSERT_EQ(host.GetBuildCallCount(), 1u);
const auto* visibleCall = host.FindBuildCall("runtime.visible");
ASSERT_NE(visibleCall, nullptr);
EXPECT_EQ(visibleCall->input.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(visibleCall->input.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(visibleCall->input.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(visibleCall->input.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(visibleCall->input.events.size(), 1u);
EXPECT_TRUE(visibleCall->input.focused);
EXPECT_EQ(host.FindBuildCall("runtime.hidden"), nullptr);
const auto* visibleLayer = FindPresentedLayerById(frame, visibleLayerId);
ASSERT_NE(visibleLayer, nullptr);
EXPECT_EQ(visibleLayer->stats.inputEventCount, 1u);
EXPECT_EQ(FindPresentedLayerById(frame, hiddenLayerId), nullptr);
}
TEST(UIRuntimeTest, UISystemConsumeLastFrameReturnsDetachedPresentationPacket) {
RecordingDocumentHost host = {};
UISystem system(host);
XCEngine::UI::Runtime::UIScreenLayerOptions options = {};
options.debugName = "runtime";
const auto layerId = system.PushScreen(
BuildScreenAsset(fs::path("runtime_consume_view.xcui"), "runtime.consume"),
options);
ASSERT_NE(layerId, 0u);
UIScreenFrameInput input = BuildInputState(12u);
input.viewportRect = XCEngine::UI::UIRect(48.0f, 72.0f, 1280.0f, 720.0f);
input.deltaTimeSeconds = 1.0 / 30.0;
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'R';
input.events.push_back(textEvent);
const auto& frame = system.Update(input);
ASSERT_EQ(frame.presentedLayerCount, 1u);
EXPECT_EQ(frame.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(frame.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(frame.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(frame.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(frame.submittedInputEventCount, 1u);
EXPECT_DOUBLE_EQ(frame.deltaTimeSeconds, input.deltaTimeSeconds);
EXPECT_TRUE(frame.focused);
XCEngine::UI::Runtime::UISystemFrameResult consumedFrame = system.ConsumeLastFrame();
EXPECT_EQ(consumedFrame.frameIndex, input.frameIndex);
EXPECT_EQ(consumedFrame.presentedLayerCount, 1u);
EXPECT_EQ(consumedFrame.layers.size(), 1u);
EXPECT_EQ(consumedFrame.layers.front().layerId, layerId);
EXPECT_EQ(consumedFrame.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(consumedFrame.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(consumedFrame.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(consumedFrame.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(consumedFrame.submittedInputEventCount, 1u);
EXPECT_DOUBLE_EQ(consumedFrame.deltaTimeSeconds, input.deltaTimeSeconds);
EXPECT_TRUE(consumedFrame.focused);
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "runtime.consume"));
const auto& clearedFrame = system.GetLastFrame();
EXPECT_EQ(clearedFrame.frameIndex, 0u);
EXPECT_EQ(clearedFrame.presentedLayerCount, 0u);
EXPECT_EQ(clearedFrame.submittedInputEventCount, 0u);
EXPECT_TRUE(clearedFrame.layers.empty());
EXPECT_EQ(clearedFrame.drawData.GetDrawListCount(), 0u);
const XCEngine::UI::Runtime::UISystemFrameResult emptyFrame = system.ConsumeLastFrame();
EXPECT_EQ(emptyFrame.frameIndex, 0u);
EXPECT_TRUE(emptyFrame.layers.empty());
EXPECT_EQ(emptyFrame.drawData.GetDrawListCount(), 0u);
}
TEST(UIRuntimeTest, SceneRuntimeContextConsumeLastFrameForwardsPresentationSnapshot) {
TempFileScope viewFile("xcui_runtime_context", ".xcui", BuildViewMarkup("Runtime Context"));
UISceneRuntimeContext runtimeContext = {};
const auto layerId = runtimeContext.GetStackController().PushMenu(
BuildScreenAsset(viewFile.Path(), "runtime.context"),
"context");
ASSERT_NE(layerId, 0u);
const XCEngine::UI::UIRect viewportRect(24.0f, 32.0f, 960.0f, 540.0f);
runtimeContext.SetViewportRect(viewportRect);
runtimeContext.SetFocused(true);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'C';
runtimeContext.QueueInputEvent(textEvent);
runtimeContext.Update(0.25);
XCEngine::UI::Runtime::UISystemFrameResult consumedFrame = runtimeContext.ConsumeLastFrame();
EXPECT_EQ(consumedFrame.frameIndex, 1u);
EXPECT_EQ(consumedFrame.presentedLayerCount, 1u);
EXPECT_EQ(consumedFrame.layers.size(), 1u);
EXPECT_EQ(consumedFrame.layers.front().layerId, layerId);
EXPECT_EQ(consumedFrame.viewportRect.x, viewportRect.x);
EXPECT_EQ(consumedFrame.viewportRect.y, viewportRect.y);
EXPECT_EQ(consumedFrame.viewportRect.width, viewportRect.width);
EXPECT_EQ(consumedFrame.viewportRect.height, viewportRect.height);
EXPECT_EQ(consumedFrame.submittedInputEventCount, 1u);
EXPECT_DOUBLE_EQ(consumedFrame.deltaTimeSeconds, 0.25);
EXPECT_TRUE(consumedFrame.focused);
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "Runtime Context"));
const auto& clearedFrame = runtimeContext.GetLastFrame();
EXPECT_EQ(clearedFrame.frameIndex, 0u);
EXPECT_EQ(clearedFrame.presentedLayerCount, 0u);
EXPECT_TRUE(clearedFrame.layers.empty());
EXPECT_EQ(clearedFrame.drawData.GetDrawListCount(), 0u);
runtimeContext.Update(0.5);
const auto& secondFrame = runtimeContext.GetLastFrame();
EXPECT_EQ(secondFrame.frameIndex, 2u);
EXPECT_EQ(secondFrame.submittedInputEventCount, 0u);
EXPECT_DOUBLE_EQ(secondFrame.deltaTimeSeconds, 0.5);
EXPECT_TRUE(secondFrame.focused);
}

View File

@@ -40,6 +40,15 @@ set(NEW_EDITOR_LAYOUT_LAB_RUNTIME_HEADER
set(NEW_EDITOR_LAYOUT_LAB_RUNTIME_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp
)
set(NEW_EDITOR_LAYOUT_LAB_PANEL_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/panels/XCUILayoutLabPanel.h
)
set(NEW_EDITOR_LAYOUT_LAB_PANEL_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/panels/XCUILayoutLabPanel.cpp
)
set(NEW_EDITOR_BASE_PANEL_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/panels/Panel.cpp
)
set(NEW_EDITOR_ASSET_DOCUMENT_SOURCE_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.h
)
@@ -64,6 +73,15 @@ set(NEW_EDITOR_COMMAND_ROUTER_HEADER
set(NEW_EDITOR_COMMAND_ROUTER_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIEditorCommandRouter.cpp
)
set(NEW_EDITOR_SHELL_CHROME_STATE_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIShellChromeState.h
)
set(NEW_EDITOR_SHELL_CHROME_STATE_SOURCE
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIShellChromeState.cpp
)
set(NEW_EDITOR_APPLICATION_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/Application.h
)
set(NEW_EDITOR_IMGUI_INPUT_ADAPTER_HEADER
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.h
)
@@ -213,6 +231,52 @@ else()
message(STATUS "Skipping new_editor_xcui_layout_lab_runtime_tests because XCUILayoutLabRuntime files are missing.")
endif()
if(EXISTS "${NEW_EDITOR_LAYOUT_LAB_PANEL_HEADER}" AND
EXISTS "${NEW_EDITOR_LAYOUT_LAB_PANEL_SOURCE}" AND
EXISTS "${NEW_EDITOR_LAYOUT_LAB_RUNTIME_HEADER}" AND
EXISTS "${NEW_EDITOR_LAYOUT_LAB_RUNTIME_SOURCE}" AND
EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE_HEADER}" AND
EXISTS "${NEW_EDITOR_ASSET_DOCUMENT_SOURCE}" AND
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_xcui_layout_lab_panel.cpp" AND
EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp")
add_executable(new_editor_xcui_layout_lab_panel_tests
test_xcui_layout_lab_panel.cpp
${NEW_EDITOR_LAYOUT_LAB_PANEL_SOURCE}
${NEW_EDITOR_BASE_PANEL_SOURCE}
${NEW_EDITOR_LAYOUT_LAB_RUNTIME_SOURCE}
${NEW_EDITOR_ASSET_DOCUMENT_SOURCE}
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp
)
xcengine_configure_new_editor_test_target(new_editor_xcui_layout_lab_panel_tests)
target_link_libraries(new_editor_xcui_layout_lab_panel_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
user32
comdlg32
)
target_include_directories(new_editor_xcui_layout_lab_panel_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
${CMAKE_BINARY_DIR}/_deps/imgui-src
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
)
target_compile_definitions(new_editor_xcui_layout_lab_panel_tests PRIVATE
XCENGINE_NEW_EDITOR_REPO_ROOT="${XCENGINE_TEST_REPO_ROOT_CMAKE}"
)
xcengine_discover_new_editor_gtests(new_editor_xcui_layout_lab_panel_tests)
else()
message(STATUS "Skipping new_editor_xcui_layout_lab_panel_tests because panel, runtime, test, or ImGui sources are missing.")
endif()
if(EXISTS "${NEW_EDITOR_BACKEND_HEADER}" AND EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp")
add_executable(new_editor_imgui_transition_backend_tests
test_new_editor_imgui_transition_backend.cpp
@@ -388,6 +452,63 @@ else()
message(STATUS "Skipping new_editor_xcui_editor_command_router_tests because command router files or the test source are missing.")
endif()
if(EXISTS "${NEW_EDITOR_SHELL_CHROME_STATE_HEADER}" AND
EXISTS "${NEW_EDITOR_SHELL_CHROME_STATE_SOURCE}" AND
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_xcui_shell_chrome_state.cpp")
add_executable(new_editor_xcui_shell_chrome_state_tests
test_xcui_shell_chrome_state.cpp
${NEW_EDITOR_SHELL_CHROME_STATE_SOURCE}
)
xcengine_configure_new_editor_test_target(new_editor_xcui_shell_chrome_state_tests)
target_link_libraries(new_editor_xcui_shell_chrome_state_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(new_editor_xcui_shell_chrome_state_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
)
xcengine_discover_new_editor_gtests(new_editor_xcui_shell_chrome_state_tests)
else()
message(STATUS "Skipping new_editor_xcui_shell_chrome_state_tests because shell chrome state files or the test source are missing.")
endif()
if(EXISTS "${NEW_EDITOR_APPLICATION_HEADER}" AND
EXISTS "${NEW_EDITOR_COMMAND_ROUTER_SOURCE}" AND
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_application_shell_command_bindings.cpp")
add_executable(new_editor_application_shell_command_bindings_tests
test_application_shell_command_bindings.cpp
${NEW_EDITOR_COMMAND_ROUTER_SOURCE}
)
xcengine_configure_new_editor_test_target(new_editor_application_shell_command_bindings_tests)
target_link_libraries(new_editor_application_shell_command_bindings_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(new_editor_application_shell_command_bindings_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_application_shell_command_bindings_tests)
else()
message(STATUS "Skipping new_editor_application_shell_command_bindings_tests because Application header, command router source, or the test source are missing.")
endif()
if(EXISTS "${NEW_EDITOR_IMGUI_INPUT_ADAPTER_HEADER}" AND
EXISTS "${NEW_EDITOR_IMGUI_INPUT_ADAPTER_SOURCE}" AND
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_imgui_xcui_input_adapter.cpp" AND
@@ -576,3 +697,63 @@ if(EXISTS "${NEW_EDITOR_RHI_COMMAND_COMPILER_HEADER}" AND
else()
message(STATUS "Skipping new_editor_xcui_rhi_command_compiler_tests because compiler files are missing.")
endif()
if(EXISTS "${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIPanelCanvasHost.h" AND
EXISTS "${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/NullXCUIPanelCanvasHost.h" AND
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_xcui_panel_canvas_host.cpp")
add_executable(new_editor_xcui_panel_canvas_host_tests
test_xcui_panel_canvas_host.cpp
)
xcengine_configure_new_editor_test_target(new_editor_xcui_panel_canvas_host_tests)
target_link_libraries(new_editor_xcui_panel_canvas_host_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(new_editor_xcui_panel_canvas_host_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
)
xcengine_discover_new_editor_gtests(new_editor_xcui_panel_canvas_host_tests)
else()
message(STATUS "Skipping new_editor_xcui_panel_canvas_host_tests because panel canvas host headers or the test source are missing.")
endif()
if(EXISTS "${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiXCUIPanelCanvasHost.h" AND
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_imgui_xcui_panel_canvas_host.cpp" AND
EXISTS "${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp")
add_executable(new_editor_imgui_xcui_panel_canvas_host_tests
test_imgui_xcui_panel_canvas_host.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_widgets.cpp
)
xcengine_configure_new_editor_test_target(new_editor_imgui_xcui_panel_canvas_host_tests)
target_link_libraries(new_editor_imgui_xcui_panel_canvas_host_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
user32
comdlg32
)
target_include_directories(new_editor_imgui_xcui_panel_canvas_host_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/new_editor/src
${CMAKE_BINARY_DIR}/_deps/imgui-src
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
)
xcengine_discover_new_editor_gtests(new_editor_imgui_xcui_panel_canvas_host_tests)
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()

View File

@@ -0,0 +1,250 @@
#include "Application.h"
#include <XCEngine/Input/InputTypes.h>
#include <gtest/gtest.h>
namespace {
using XCEngine::Editor::XCUIBackend::XCUIEditorCommandRouter;
using XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta;
using XCEngine::Input::KeyCode;
using XCEngine::NewEditor::Application;
constexpr std::size_t ToPanelIndex(Application::ShellPanelId panelId) {
return static_cast<std::size_t>(panelId);
}
struct ShellCommandHarness {
Application::ShellViewToggleState viewToggles = {};
std::array<Application::ShellPanelChromeState, static_cast<std::size_t>(Application::ShellPanelId::Count)>
panels = {};
int hostedPreviewReconfigureCount = 0;
ShellCommandHarness() {
panels[ToPanelIndex(Application::ShellPanelId::XCUIDemo)] = {
Application::ShellPanelId::XCUIDemo,
"XCUI Demo",
"XCUI Demo",
"new_editor.panels.xcui_demo",
true,
true,
Application::ShellHostedPreviewMode::NativeOffscreen
};
panels[ToPanelIndex(Application::ShellPanelId::XCUILayoutLab)] = {
Application::ShellPanelId::XCUILayoutLab,
"XCUI Layout Lab",
"XCUI Layout Lab",
"new_editor.panels.xcui_layout_lab",
true,
true,
Application::ShellHostedPreviewMode::LegacyImGui
};
}
Application::ShellPanelChromeState& Panel(Application::ShellPanelId panelId) {
return panels[ToPanelIndex(panelId)];
}
const Application::ShellPanelChromeState& Panel(Application::ShellPanelId panelId) const {
return panels[ToPanelIndex(panelId)];
}
Application::ShellCommandBindings BuildBindings() {
Application::ShellCommandBindings bindings = {};
bindings.getXCUIDemoPanelVisible = [this]() { return Panel(Application::ShellPanelId::XCUIDemo).visible; };
bindings.setXCUIDemoPanelVisible = [this](bool visible) {
Panel(Application::ShellPanelId::XCUIDemo).visible = visible;
};
bindings.getXCUILayoutLabPanelVisible = [this]() {
return Panel(Application::ShellPanelId::XCUILayoutLab).visible;
};
bindings.setXCUILayoutLabPanelVisible = [this](bool visible) {
Panel(Application::ShellPanelId::XCUILayoutLab).visible = visible;
};
bindings.getImGuiDemoWindowVisible = [this]() { return viewToggles.imguiDemoWindowVisible; };
bindings.setImGuiDemoWindowVisible = [this](bool visible) { viewToggles.imguiDemoWindowVisible = visible; };
bindings.getNativeBackdropVisible = [this]() { return viewToggles.nativeBackdropVisible; };
bindings.setNativeBackdropVisible = [this](bool visible) { viewToggles.nativeBackdropVisible = visible; };
bindings.getPulseAccentEnabled = [this]() { return viewToggles.pulseAccentEnabled; };
bindings.setPulseAccentEnabled = [this](bool enabled) { viewToggles.pulseAccentEnabled = enabled; };
bindings.getNativeXCUIOverlayVisible = [this]() { return viewToggles.nativeXCUIOverlayVisible; };
bindings.setNativeXCUIOverlayVisible = [this](bool visible) { viewToggles.nativeXCUIOverlayVisible = visible; };
bindings.getHostedPreviewHudVisible = [this]() { return viewToggles.hostedPreviewHudVisible; };
bindings.setHostedPreviewHudVisible = [this](bool visible) { viewToggles.hostedPreviewHudVisible = visible; };
bindings.getNativeDemoPanelPreviewEnabled = [this]() {
return Panel(Application::ShellPanelId::XCUIDemo).previewMode ==
Application::ShellHostedPreviewMode::NativeOffscreen;
};
bindings.setNativeDemoPanelPreviewEnabled = [this](bool enabled) {
Panel(Application::ShellPanelId::XCUIDemo).previewMode =
enabled
? Application::ShellHostedPreviewMode::NativeOffscreen
: Application::ShellHostedPreviewMode::LegacyImGui;
};
bindings.getNativeLayoutLabPreviewEnabled = [this]() {
return Panel(Application::ShellPanelId::XCUILayoutLab).previewMode ==
Application::ShellHostedPreviewMode::NativeOffscreen;
};
bindings.setNativeLayoutLabPreviewEnabled = [this](bool enabled) {
Panel(Application::ShellPanelId::XCUILayoutLab).previewMode =
enabled
? Application::ShellHostedPreviewMode::NativeOffscreen
: Application::ShellHostedPreviewMode::LegacyImGui;
};
bindings.onHostedPreviewModeChanged = [this]() { ++hostedPreviewReconfigureCount; };
return bindings;
}
};
TEST(ApplicationShellCommandBindingsTest, RegisterShellViewCommandsInvokesBoundToggleHandlers) {
ShellCommandHarness harness = {};
XCUIEditorCommandRouter router = {};
Application::RegisterShellViewCommands(router, harness.BuildBindings());
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleXCUIDemoPanel));
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleXCUILayoutLabPanel));
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleImGuiDemoWindow));
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleNativeBackdrop));
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::TogglePulseAccent));
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleNativeXCUIOverlay));
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleHostedPreviewHud));
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleNativeDemoPanelPreview));
EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleNativeLayoutLabPreview));
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleXCUIDemoPanel));
EXPECT_FALSE(harness.Panel(Application::ShellPanelId::XCUIDemo).visible);
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleImGuiDemoWindow));
EXPECT_TRUE(harness.viewToggles.imguiDemoWindowVisible);
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleNativeBackdrop));
EXPECT_FALSE(harness.viewToggles.nativeBackdropVisible);
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleNativeXCUIOverlay));
EXPECT_FALSE(harness.viewToggles.nativeXCUIOverlayVisible);
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleHostedPreviewHud));
EXPECT_FALSE(harness.viewToggles.hostedPreviewHudVisible);
}
TEST(ApplicationShellCommandBindingsTest, PreviewModeCommandsTriggerHostedPreviewReconfigureCallback) {
ShellCommandHarness harness = {};
XCUIEditorCommandRouter router = {};
Application::RegisterShellViewCommands(router, harness.BuildBindings());
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleNativeDemoPanelPreview));
EXPECT_EQ(
harness.Panel(Application::ShellPanelId::XCUIDemo).previewMode,
Application::ShellHostedPreviewMode::LegacyImGui);
EXPECT_EQ(harness.hostedPreviewReconfigureCount, 1);
EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleNativeLayoutLabPreview));
EXPECT_EQ(
harness.Panel(Application::ShellPanelId::XCUILayoutLab).previewMode,
Application::ShellHostedPreviewMode::NativeOffscreen);
EXPECT_EQ(harness.hostedPreviewReconfigureCount, 2);
}
TEST(ApplicationShellCommandBindingsTest, BuildShellShortcutSnapshotCarriesBridgeStateAndKeyboardEdges) {
XCUIInputBridgeFrameDelta frameDelta = {};
frameDelta.state.windowFocused = true;
frameDelta.state.wantCaptureKeyboard = true;
frameDelta.state.wantTextInput = true;
frameDelta.state.modifiers.control = true;
frameDelta.state.modifiers.shift = true;
frameDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
frameDelta.keyboard.repeatedKeys.push_back(static_cast<std::int32_t>(KeyCode::Two));
const auto snapshot = Application::BuildShellShortcutSnapshot(frameDelta);
EXPECT_TRUE(snapshot.windowFocused);
EXPECT_TRUE(snapshot.wantCaptureKeyboard);
EXPECT_TRUE(snapshot.wantTextInput);
EXPECT_TRUE(snapshot.modifiers.control);
EXPECT_TRUE(snapshot.modifiers.shift);
EXPECT_TRUE(snapshot.IsKeyDown(static_cast<std::int32_t>(KeyCode::One)));
EXPECT_TRUE(snapshot.IsKeyDown(static_cast<std::int32_t>(KeyCode::Two)));
const auto* repeatedKey = snapshot.FindKeyState(static_cast<std::int32_t>(KeyCode::Two));
ASSERT_NE(repeatedKey, nullptr);
EXPECT_TRUE(repeatedKey->repeat);
}
TEST(ApplicationShellCommandBindingsTest, RegisteredShellShortcutsMatchExpectedViewCommands) {
ShellCommandHarness harness = {};
XCUIEditorCommandRouter router = {};
Application::RegisterShellViewCommands(router, harness.BuildBindings());
XCUIInputBridgeFrameDelta frameDelta = {};
frameDelta.state.windowFocused = true;
frameDelta.state.modifiers.control = true;
frameDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
const auto demoSnapshot = Application::BuildShellShortcutSnapshot(frameDelta);
const auto demoMatch = router.MatchShortcut({ &demoSnapshot });
ASSERT_TRUE(demoMatch.matched);
EXPECT_EQ(demoMatch.commandId, Application::ShellCommandIds::ToggleXCUIDemoPanel);
XCUIInputBridgeFrameDelta previewDelta = {};
previewDelta.state.windowFocused = true;
previewDelta.state.modifiers.control = true;
previewDelta.state.modifiers.alt = true;
previewDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::Two));
const auto previewSnapshot = Application::BuildShellShortcutSnapshot(previewDelta);
const auto previewMatch = router.MatchShortcut({ &previewSnapshot });
ASSERT_TRUE(previewMatch.matched);
EXPECT_EQ(previewMatch.commandId, Application::ShellCommandIds::ToggleNativeLayoutLabPreview);
}
TEST(ApplicationShellCommandBindingsTest, RegisteredShellShortcutsRespectCaptureAndRepeatGuards) {
ShellCommandHarness harness = {};
XCUIEditorCommandRouter router = {};
Application::RegisterShellViewCommands(router, harness.BuildBindings());
XCUIInputBridgeFrameDelta capturedDelta = {};
capturedDelta.state.windowFocused = true;
capturedDelta.state.wantCaptureKeyboard = true;
capturedDelta.state.modifiers.control = true;
capturedDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
const auto capturedSnapshot = Application::BuildShellShortcutSnapshot(capturedDelta);
EXPECT_FALSE(router.MatchShortcut({ &capturedSnapshot }).matched);
XCUIInputBridgeFrameDelta textInputDelta = {};
textInputDelta.state.windowFocused = true;
textInputDelta.state.wantTextInput = true;
textInputDelta.state.modifiers.control = true;
textInputDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
const auto textInputSnapshot = Application::BuildShellShortcutSnapshot(textInputDelta);
EXPECT_FALSE(router.MatchShortcut({ &textInputSnapshot }).matched);
XCUIInputBridgeFrameDelta repeatedDelta = {};
repeatedDelta.state.windowFocused = true;
repeatedDelta.state.modifiers.control = true;
repeatedDelta.keyboard.repeatedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
const auto repeatedSnapshot = Application::BuildShellShortcutSnapshot(repeatedDelta);
EXPECT_FALSE(router.MatchShortcut({ &repeatedSnapshot }).matched);
}
TEST(ApplicationShellCommandBindingsTest, PreviewShortcutInvokesCommandHandlerAndReconfigureCallback) {
ShellCommandHarness harness = {};
XCUIEditorCommandRouter router = {};
Application::RegisterShellViewCommands(router, harness.BuildBindings());
XCUIInputBridgeFrameDelta previewDelta = {};
previewDelta.state.windowFocused = true;
previewDelta.state.modifiers.control = true;
previewDelta.state.modifiers.alt = true;
previewDelta.keyboard.pressedKeys.push_back(static_cast<std::int32_t>(KeyCode::One));
const auto previewSnapshot = Application::BuildShellShortcutSnapshot(previewDelta);
EXPECT_TRUE(router.InvokeMatchingShortcut({ &previewSnapshot }));
EXPECT_EQ(
harness.Panel(Application::ShellPanelId::XCUIDemo).previewMode,
Application::ShellHostedPreviewMode::LegacyImGui);
EXPECT_EQ(harness.hostedPreviewReconfigureCount, 1);
}
} // namespace

View File

@@ -0,0 +1,25 @@
#include <gtest/gtest.h>
#include "XCUIBackend/ImGuiXCUIPanelCanvasHost.h"
namespace {
using XCEngine::Editor::XCUIBackend::CreateImGuiXCUIPanelCanvasHost;
using XCEngine::Editor::XCUIBackend::IXCUIPanelCanvasHost;
using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasHostBackend;
using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasHostCapabilities;
TEST(NewEditorImGuiXCUIPanelCanvasHostTest, ReportsExplicitBackendAndCapabilities) {
std::unique_ptr<IXCUIPanelCanvasHost> host = CreateImGuiXCUIPanelCanvasHost();
ASSERT_NE(host, nullptr);
EXPECT_STREQ(host->GetDebugName(), "ImGuiXCUIPanelCanvasHost");
EXPECT_EQ(host->GetBackend(), XCUIPanelCanvasHostBackend::ImGui);
const XCUIPanelCanvasHostCapabilities capabilities = host->GetCapabilities();
EXPECT_TRUE(capabilities.supportsPointerHitTesting);
EXPECT_TRUE(capabilities.supportsHostedSurfaceImages);
EXPECT_TRUE(capabilities.supportsPrimitiveOverlays);
}
} // namespace

View File

@@ -56,6 +56,17 @@ UIInputEvent MakeKeyDownEvent(
return event;
}
UIInputEvent MakePointerButtonEvent(
UIInputEventType type,
const XCEngine::UI::UIPoint& position,
XCEngine::UI::UIPointerButton button = XCEngine::UI::UIPointerButton::Left) {
UIInputEvent event = {};
event.type = type;
event.pointerButton = button;
event.position = position;
return event;
}
fs::path FindDemoResourcePath() {
fs::path probe = fs::current_path();
for (int i = 0; i < 8; ++i) {
@@ -263,6 +274,25 @@ TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsCapturesPointerActivati
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
}
TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsCapturesShortcutCommands) {
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baselineFrame = runtime.Update(BuildInputState());
ASSERT_TRUE(baselineFrame.stats.documentsReady);
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
XCEngine::Editor::XCUIBackend::XCUIDemoInputState shortcutInput = BuildInputState();
shortcutInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::P, false, false, true));
const auto& shortcutFrame = runtime.Update(shortcutInput);
ASSERT_TRUE(shortcutFrame.stats.documentsReady);
EXPECT_TRUE(shortcutFrame.stats.accentEnabled);
EXPECT_EQ(shortcutFrame.stats.lastCommandId, "demo.toggleAccent");
EXPECT_EQ(runtime.DrainPendingCommandIds(), std::vector<std::string>({ "demo.toggleAccent" }));
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
}
TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsPreservesMultipleTextEditCommandsPerFrame) {
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
@@ -313,6 +343,43 @@ TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsPreservesMultipleTextEd
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
}
TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsPreserveMixedPointerTextAndShortcutOrder) {
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baselineFrame = runtime.Update(BuildInputState());
ASSERT_TRUE(baselineFrame.stats.documentsReady);
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
XCEngine::UI::UIRect promptRect = {};
ASSERT_TRUE(runtime.TryGetElementRect("agentPrompt", promptRect));
const XCEngine::UI::UIPoint promptCenter(
promptRect.x + promptRect.width * 0.5f,
promptRect.y + promptRect.height * 0.5f);
XCEngine::Editor::XCUIBackend::XCUIDemoInputState mixedInput = BuildInputState();
mixedInput.pointerPosition = promptCenter;
mixedInput.events.push_back(MakePointerButtonEvent(UIInputEventType::PointerButtonDown, promptCenter));
mixedInput.events.push_back(MakePointerButtonEvent(UIInputEventType::PointerButtonUp, promptCenter));
mixedInput.events.push_back(MakeCharacterEvent('A'));
mixedInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::P, false, false, true));
const auto& mixedFrame = runtime.Update(mixedInput);
ASSERT_TRUE(mixedFrame.stats.documentsReady);
EXPECT_EQ(mixedFrame.stats.focusedElementId, "agentPrompt");
EXPECT_TRUE(mixedFrame.stats.accentEnabled);
EXPECT_EQ(mixedFrame.stats.lastCommandId, "demo.toggleAccent");
EXPECT_NE(FindTextCommand(mixedFrame.drawData, "A"), nullptr);
EXPECT_EQ(
runtime.DrainPendingCommandIds(),
std::vector<std::string>({
"demo.activate.agentPrompt",
"demo.text.edit.agentPrompt",
"demo.toggleAccent" }));
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
}
TEST(NewEditorXCUIDemoRuntimeTest, PointerToggleUpdatesFocusStatusTextAndAccentState) {
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());

View File

@@ -13,6 +13,7 @@ namespace {
using XCEngine::Editor::XCUIBackend::CreateImGuiXCUIHostedPreviewPresenter;
using XCEngine::Editor::XCUIBackend::CreateQueuedNativeXCUIHostedPreviewPresenter;
using XCEngine::Editor::XCUIBackend::IImGuiXCUIHostedPreviewTargetBinding;
using XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame;
using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewDrainStats;
@@ -36,6 +37,19 @@ public:
}
};
class RecordingImGuiHostedPreviewTargetBinding final : public IImGuiXCUIHostedPreviewTargetBinding {
public:
ImDrawList* ResolveTargetDrawList(const XCUIHostedPreviewFrame& frame) const override {
++resolveCallCount;
lastFrame = &frame;
return resolvedDrawList;
}
mutable std::size_t resolveCallCount = 0u;
mutable const XCUIHostedPreviewFrame* lastFrame = nullptr;
ImDrawList* resolvedDrawList = nullptr;
};
void PrepareImGui(float width = 1024.0f, float height = 768.0f) {
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize = ImVec2(width, height);
@@ -78,13 +92,10 @@ TEST(XCUIHostedPreviewPresenterTest, PresentReturnsFalseAndClearsStatsWhenFrameH
EXPECT_EQ(stats.flushedCommandCount, 0u);
}
TEST(XCUIHostedPreviewPresenterTest, PresentFlushesDrawDataIntoProvidedImGuiDrawList) {
TEST(XCUIHostedPreviewPresenterTest, PresentFlushesDrawDataIntoExplicitBindingResolvedImGuiDrawList) {
ImGuiContextScope contextScope;
PrepareImGui(800.0f, 600.0f);
std::unique_ptr<IXCUIHostedPreviewPresenter> presenter = CreateImGuiXCUIHostedPreviewPresenter();
ASSERT_NE(presenter, nullptr);
XCEngine::UI::UIDrawData drawData = {};
XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("HostedPreview");
drawList.AddFilledRect(
@@ -101,6 +112,15 @@ TEST(XCUIHostedPreviewPresenterTest, PresentFlushesDrawDataIntoProvidedImGuiDraw
ImDrawList* targetDrawList = ImGui::GetWindowDrawList();
ASSERT_NE(targetDrawList, nullptr);
std::unique_ptr<RecordingImGuiHostedPreviewTargetBinding> targetBinding =
std::make_unique<RecordingImGuiHostedPreviewTargetBinding>();
RecordingImGuiHostedPreviewTargetBinding* targetBindingPtr = targetBinding.get();
targetBindingPtr->resolvedDrawList = targetDrawList;
std::unique_ptr<IXCUIHostedPreviewPresenter> presenter =
CreateImGuiXCUIHostedPreviewPresenter(std::move(targetBinding));
ASSERT_NE(presenter, nullptr);
XCUIHostedPreviewFrame frame = {};
frame.drawData = &drawData;
@@ -116,6 +136,129 @@ TEST(XCUIHostedPreviewPresenterTest, PresentFlushesDrawDataIntoProvidedImGuiDraw
EXPECT_EQ(stats.submittedCommandCount, 2u);
EXPECT_EQ(stats.flushedDrawListCount, 1u);
EXPECT_EQ(stats.flushedCommandCount, 2u);
EXPECT_EQ(targetBindingPtr->resolveCallCount, 1u);
EXPECT_EQ(targetBindingPtr->lastFrame, &frame);
EXPECT_GT(targetDrawList->VtxBuffer.Size, 0);
EXPECT_GT(targetDrawList->CmdBuffer.Size, 0);
}
TEST(XCUIHostedPreviewPresenterTest, PresentReturnsFalseWhenExplicitBindingDoesNotResolveTargetDrawList) {
ImGuiContextScope contextScope;
PrepareImGui(800.0f, 600.0f);
XCEngine::UI::UIDrawData drawData = {};
drawData.EmplaceDrawList("HostedPreviewMissingTarget").AddFilledRect(
XCEngine::UI::UIRect(10.0f, 12.0f, 44.0f, 28.0f),
XCEngine::UI::UIColor(0.9f, 0.3f, 0.2f, 1.0f));
std::unique_ptr<RecordingImGuiHostedPreviewTargetBinding> targetBinding =
std::make_unique<RecordingImGuiHostedPreviewTargetBinding>();
RecordingImGuiHostedPreviewTargetBinding* targetBindingPtr = targetBinding.get();
std::unique_ptr<IXCUIHostedPreviewPresenter> presenter =
CreateImGuiXCUIHostedPreviewPresenter(std::move(targetBinding));
ASSERT_NE(presenter, nullptr);
XCUIHostedPreviewFrame frame = {};
frame.drawData = &drawData;
frame.debugName = "HostedPreviewMissingTarget";
const bool presented = presenter->Present(frame);
const XCUIHostedPreviewStats& stats = presenter->GetLastStats();
EXPECT_FALSE(presented);
EXPECT_FALSE(stats.presented);
EXPECT_EQ(stats.submittedDrawListCount, 1u);
EXPECT_EQ(stats.submittedCommandCount, 1u);
EXPECT_EQ(stats.flushedDrawListCount, 0u);
EXPECT_EQ(stats.flushedCommandCount, 0u);
EXPECT_EQ(targetBindingPtr->resolveCallCount, 1u);
EXPECT_EQ(targetBindingPtr->lastFrame, &frame);
}
TEST(XCUIHostedPreviewPresenterTest, PresentFlushesDrawDataIntoExplicitBindingResolvedForegroundDrawListWithoutWindow) {
ImGuiContextScope contextScope;
PrepareImGui(800.0f, 600.0f);
XCEngine::UI::UIDrawData drawData = {};
XCEngine::UI::UIDrawList& drawList = drawData.EmplaceDrawList("HostedPreviewForegroundTarget");
drawList.AddFilledRect(
XCEngine::UI::UIRect(16.0f, 18.0f, 52.0f, 34.0f),
XCEngine::UI::UIColor(0.15f, 0.75f, 0.45f, 1.0f));
drawList.AddText(
XCEngine::UI::UIPoint(24.0f, 28.0f),
"foreground",
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
15.0f);
ImGui::NewFrame();
ImDrawList* targetDrawList = ImGui::GetForegroundDrawList();
ASSERT_NE(targetDrawList, nullptr);
const int baselineVertexCount = targetDrawList->VtxBuffer.Size;
const int baselineIndexCount = targetDrawList->IdxBuffer.Size;
const int baselineCommandCount = targetDrawList->CmdBuffer.Size;
std::unique_ptr<RecordingImGuiHostedPreviewTargetBinding> targetBinding =
std::make_unique<RecordingImGuiHostedPreviewTargetBinding>();
RecordingImGuiHostedPreviewTargetBinding* targetBindingPtr = targetBinding.get();
targetBindingPtr->resolvedDrawList = targetDrawList;
std::unique_ptr<IXCUIHostedPreviewPresenter> presenter =
CreateImGuiXCUIHostedPreviewPresenter(std::move(targetBinding));
ASSERT_NE(presenter, nullptr);
XCUIHostedPreviewFrame frame = {};
frame.drawData = &drawData;
frame.debugName = "HostedPreviewForegroundTarget";
const bool presented = presenter->Present(frame);
const XCUIHostedPreviewStats& stats = presenter->GetLastStats();
ImGui::EndFrame();
EXPECT_TRUE(presented);
EXPECT_TRUE(stats.presented);
EXPECT_EQ(stats.submittedDrawListCount, 1u);
EXPECT_EQ(stats.submittedCommandCount, 2u);
EXPECT_EQ(stats.flushedDrawListCount, 1u);
EXPECT_EQ(stats.flushedCommandCount, 2u);
EXPECT_EQ(targetBindingPtr->resolveCallCount, 1u);
EXPECT_EQ(targetBindingPtr->lastFrame, &frame);
EXPECT_GT(targetDrawList->VtxBuffer.Size, baselineVertexCount);
EXPECT_GT(targetDrawList->IdxBuffer.Size, baselineIndexCount);
EXPECT_GE(targetDrawList->CmdBuffer.Size, baselineCommandCount);
}
TEST(XCUIHostedPreviewPresenterTest, DefaultFactoryStillUsesCurrentWindowBindingForLegacyImGuiPath) {
ImGuiContextScope contextScope;
PrepareImGui(800.0f, 600.0f);
std::unique_ptr<IXCUIHostedPreviewPresenter> presenter = CreateImGuiXCUIHostedPreviewPresenter();
ASSERT_NE(presenter, nullptr);
XCEngine::UI::UIDrawData drawData = {};
drawData.EmplaceDrawList("HostedPreviewDefaultBinding").AddFilledRect(
XCEngine::UI::UIRect(8.0f, 10.0f, 36.0f, 22.0f),
XCEngine::UI::UIColor(0.2f, 0.6f, 0.9f, 1.0f));
ImGui::NewFrame();
ASSERT_TRUE(ImGui::Begin("HostedPreviewPresenterDefaultBindingWindow"));
ImDrawList* targetDrawList = ImGui::GetWindowDrawList();
ASSERT_NE(targetDrawList, nullptr);
XCUIHostedPreviewFrame frame = {};
frame.drawData = &drawData;
const bool presented = presenter->Present(frame);
const XCUIHostedPreviewStats& stats = presenter->GetLastStats();
ImGui::End();
ImGui::EndFrame();
EXPECT_TRUE(presented);
EXPECT_TRUE(stats.presented);
EXPECT_EQ(stats.flushedDrawListCount, 1u);
EXPECT_EQ(stats.flushedCommandCount, 1u);
EXPECT_GT(targetDrawList->VtxBuffer.Size, 0);
EXPECT_GT(targetDrawList->CmdBuffer.Size, 0);
}

View File

@@ -22,6 +22,20 @@ XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState BuildInputState(
return input;
}
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState BuildKeyboardInputState(
float width = 960.0f,
float height = 640.0f) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState input = BuildInputState(width, height);
input.pointerInside = false;
return input;
}
XCEngine::UI::UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
return XCEngine::UI::UIPoint(
rect.x + rect.width * 0.5f,
rect.y + rect.height * 0.5f);
}
std::vector<const UIDrawCommand*> CollectTextCommands(const XCEngine::UI::UIDrawData& drawData) {
std::vector<const UIDrawCommand*> textCommands = {};
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
@@ -363,3 +377,156 @@ TEST(NewEditorXCUILayoutLabRuntimeTest, ClickingPropertySectionHeaderTogglesFiel
EXPECT_TRUE(runtime.TryGetElementRect("fieldPosition", fieldRect));
EXPECT_GT(fieldRect.height, 0.0f);
}
TEST(NewEditorXCUILayoutLabRuntimeTest, KeyboardNavigationMovesSelectionAcrossListItems) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baseline = runtime.Update(BuildInputState());
ASSERT_TRUE(baseline.stats.documentsReady);
XCEngine::UI::UIRect listItemRect = {};
ASSERT_TRUE(runtime.TryGetElementRect("assetLighting", listItemRect));
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState selectInput = BuildInputState();
selectInput.pointerPosition = RectCenter(listItemRect);
selectInput.pointerPressed = true;
const auto& selectedFrame = runtime.Update(selectInput);
ASSERT_TRUE(selectedFrame.stats.documentsReady);
EXPECT_EQ(selectedFrame.stats.selectedElementId, "assetLighting");
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState nextInput = BuildKeyboardInputState();
nextInput.navigateNext = true;
const auto& nextFrame = runtime.Update(nextInput);
ASSERT_TRUE(nextFrame.stats.documentsReady);
EXPECT_EQ(nextFrame.stats.selectedElementId, "assetMaterials");
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState endInput = BuildKeyboardInputState();
endInput.navigateEnd = true;
const auto& endFrame = runtime.Update(endInput);
ASSERT_TRUE(endFrame.stats.documentsReady);
ASSERT_FALSE(endFrame.stats.selectedElementId.empty());
EXPECT_NE(endFrame.stats.selectedElementId, "assetLighting");
const std::string lastListSelection = endFrame.stats.selectedElementId;
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState nextAtEndInput = BuildKeyboardInputState();
nextAtEndInput.navigateNext = true;
const auto& nextAtEndFrame = runtime.Update(nextAtEndInput);
ASSERT_TRUE(nextAtEndFrame.stats.documentsReady);
EXPECT_EQ(nextAtEndFrame.stats.selectedElementId, lastListSelection);
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState homeInput = BuildKeyboardInputState();
homeInput.navigateHome = true;
const auto& homeFrame = runtime.Update(homeInput);
ASSERT_TRUE(homeFrame.stats.documentsReady);
EXPECT_EQ(homeFrame.stats.selectedElementId, "assetLighting");
}
TEST(NewEditorXCUILayoutLabRuntimeTest, KeyboardCollapseAndExpandFollowTreeHierarchy) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baseline = runtime.Update(BuildInputState());
ASSERT_TRUE(baseline.stats.documentsReady);
XCEngine::UI::UIRect treeChildRect = {};
ASSERT_TRUE(runtime.TryGetElementRect("treeScenes", treeChildRect));
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState selectInput = BuildInputState();
selectInput.pointerPosition = RectCenter(treeChildRect);
selectInput.pointerPressed = true;
const auto& selectedFrame = runtime.Update(selectInput);
ASSERT_TRUE(selectedFrame.stats.documentsReady);
EXPECT_EQ(selectedFrame.stats.selectedElementId, "treeScenes");
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState collapseToParent = BuildKeyboardInputState();
collapseToParent.navigateCollapse = true;
const auto& parentFrame = runtime.Update(collapseToParent);
ASSERT_TRUE(parentFrame.stats.documentsReady);
EXPECT_EQ(parentFrame.stats.selectedElementId, "treeAssetsRoot");
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState collapseRoot = BuildKeyboardInputState();
collapseRoot.navigateCollapse = true;
const auto& collapsedFrame = runtime.Update(collapseRoot);
ASSERT_TRUE(collapsedFrame.stats.documentsReady);
EXPECT_EQ(collapsedFrame.stats.selectedElementId, "treeAssetsRoot");
const auto& collapsedPersistedFrame = runtime.Update(BuildKeyboardInputState());
ASSERT_TRUE(collapsedPersistedFrame.stats.documentsReady);
EXPECT_EQ(collapsedPersistedFrame.stats.expandedTreeItemCount, 0u);
EXPECT_FALSE(runtime.TryGetElementRect("treeScenes", treeChildRect));
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState expandRoot = BuildKeyboardInputState();
expandRoot.navigateExpand = true;
const auto& expandedRootFrame = runtime.Update(expandRoot);
ASSERT_TRUE(expandedRootFrame.stats.documentsReady);
EXPECT_EQ(expandedRootFrame.stats.selectedElementId, "treeAssetsRoot");
const auto& expandedPersistedFrame = runtime.Update(BuildKeyboardInputState());
ASSERT_TRUE(expandedPersistedFrame.stats.documentsReady);
EXPECT_EQ(expandedPersistedFrame.stats.expandedTreeItemCount, 1u);
ASSERT_TRUE(runtime.TryGetElementRect("treeScenes", treeChildRect));
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState moveIntoChild = BuildKeyboardInputState();
moveIntoChild.navigateExpand = true;
const auto& childFrame = runtime.Update(moveIntoChild);
ASSERT_TRUE(childFrame.stats.documentsReady);
EXPECT_EQ(childFrame.stats.selectedElementId, "treeScenes");
}
TEST(NewEditorXCUILayoutLabRuntimeTest, KeyboardNavigationTraversesPropertySectionsAndFields) {
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
ASSERT_TRUE(runtime.ReloadDocuments());
const auto& baseline = runtime.Update(BuildInputState());
ASSERT_TRUE(baseline.stats.documentsReady);
XCEngine::UI::UIRect sectionRect = {};
ASSERT_TRUE(runtime.TryGetElementRect("inspectorTransform", sectionRect));
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState selectInput = BuildInputState();
selectInput.pointerPosition = XCEngine::UI::UIPoint(sectionRect.x + 18.0f, sectionRect.y + 10.0f);
selectInput.pointerPressed = true;
const auto& selectedFrame = runtime.Update(selectInput);
ASSERT_TRUE(selectedFrame.stats.documentsReady);
EXPECT_EQ(selectedFrame.stats.selectedElementId, "inspectorTransform");
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState nextSectionInput = BuildKeyboardInputState();
nextSectionInput.navigateNext = true;
const auto& nextSectionFrame = runtime.Update(nextSectionInput);
ASSERT_TRUE(nextSectionFrame.stats.documentsReady);
EXPECT_EQ(nextSectionFrame.stats.selectedElementId, "inspectorMesh");
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState previousSectionInput = BuildKeyboardInputState();
previousSectionInput.navigatePrevious = true;
const auto& previousSectionFrame = runtime.Update(previousSectionInput);
ASSERT_TRUE(previousSectionFrame.stats.documentsReady);
EXPECT_EQ(previousSectionFrame.stats.selectedElementId, "inspectorTransform");
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState expandIntoFieldsInput = BuildKeyboardInputState();
expandIntoFieldsInput.navigateExpand = true;
const auto& expandedSectionFrame = runtime.Update(expandIntoFieldsInput);
ASSERT_TRUE(expandedSectionFrame.stats.documentsReady);
EXPECT_EQ(expandedSectionFrame.stats.selectedElementId, "inspectorTransform");
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState enterFieldsInput = BuildKeyboardInputState();
enterFieldsInput.navigateExpand = true;
const auto& firstFieldFrame = runtime.Update(enterFieldsInput);
ASSERT_TRUE(firstFieldFrame.stats.documentsReady);
EXPECT_EQ(firstFieldFrame.stats.selectedElementId, "fieldPosition");
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState nextFieldInput = BuildKeyboardInputState();
nextFieldInput.navigateNext = true;
const auto& nextFieldFrame = runtime.Update(nextFieldInput);
ASSERT_TRUE(nextFieldFrame.stats.documentsReady);
EXPECT_EQ(nextFieldFrame.stats.selectedElementId, "fieldRotation");
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState collapseToSectionInput = BuildKeyboardInputState();
collapseToSectionInput.navigateCollapse = true;
const auto& collapseToSectionFrame = runtime.Update(collapseToSectionInput);
ASSERT_TRUE(collapseToSectionFrame.stats.documentsReady);
EXPECT_EQ(collapseToSectionFrame.stats.selectedElementId, "inspectorTransform");
}

View File

@@ -0,0 +1,67 @@
#include <gtest/gtest.h>
#include "XCUIBackend/NullXCUIPanelCanvasHost.h"
namespace {
using XCEngine::Editor::XCUIBackend::CreateNullXCUIPanelCanvasHost;
using XCEngine::Editor::XCUIBackend::IXCUIPanelCanvasHost;
using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasHostBackend;
using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasHostCapabilities;
using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasRequest;
using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession;
TEST(NewEditorXCUIPanelCanvasHostTest, NullHostReportsExplicitBackendAndCapabilities) {
std::unique_ptr<IXCUIPanelCanvasHost> host = CreateNullXCUIPanelCanvasHost();
ASSERT_NE(host, nullptr);
EXPECT_STREQ(host->GetDebugName(), "NullXCUIPanelCanvasHost");
EXPECT_EQ(host->GetBackend(), XCUIPanelCanvasHostBackend::Null);
const XCUIPanelCanvasHostCapabilities capabilities = host->GetCapabilities();
EXPECT_FALSE(capabilities.supportsPointerHitTesting);
EXPECT_FALSE(capabilities.supportsHostedSurfaceImages);
EXPECT_FALSE(capabilities.supportsPrimitiveOverlays);
}
TEST(NewEditorXCUIPanelCanvasHostTest, NullHostBeginCanvasReturnsEmptySessionAndDrawCallsAreNoops) {
std::unique_ptr<IXCUIPanelCanvasHost> host = CreateNullXCUIPanelCanvasHost();
ASSERT_NE(host, nullptr);
XCUIPanelCanvasRequest request = {};
request.childId = "NullCanvas";
request.height = 280.0f;
request.topInset = 24.0f;
request.bordered = true;
request.showSurfaceImage = true;
request.drawPreviewFrame = true;
request.placeholderTitle = "Placeholder";
request.badgeTitle = "Badge";
const XCUIPanelCanvasSession session = host->BeginCanvas(request);
EXPECT_FALSE(session.validCanvas);
EXPECT_FALSE(session.hovered);
EXPECT_FALSE(session.windowFocused);
EXPECT_FLOAT_EQ(session.hostRect.width, 0.0f);
EXPECT_FLOAT_EQ(session.hostRect.height, 0.0f);
EXPECT_FLOAT_EQ(session.canvasRect.width, 0.0f);
EXPECT_FLOAT_EQ(session.canvasRect.height, 0.0f);
host->DrawFilledRect(
XCEngine::UI::UIRect(10.0f, 12.0f, 48.0f, 64.0f),
XCEngine::UI::UIColor(1.0f, 0.0f, 0.0f, 1.0f),
6.0f);
host->DrawOutlineRect(
XCEngine::UI::UIRect(5.0f, 6.0f, 100.0f, 40.0f),
XCEngine::UI::UIColor(0.0f, 1.0f, 0.0f, 1.0f),
2.0f,
8.0f);
host->DrawText(
XCEngine::UI::UIPoint(8.0f, 14.0f),
"Null host should ignore text draws",
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
16.0f);
host->EndCanvas();
}
} // namespace

View File

@@ -0,0 +1,221 @@
#include "XCUIBackend/XCUIShellChromeState.h"
#include <gtest/gtest.h>
namespace {
using XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds;
using XCEngine::Editor::XCUIBackend::XCUIShellChromeState;
using XCEngine::Editor::XCUIBackend::XCUIShellHostedPreviewMode;
using XCEngine::Editor::XCUIBackend::XCUIShellHostedPreviewState;
using XCEngine::Editor::XCUIBackend::XCUIShellPanelId;
using XCEngine::Editor::XCUIBackend::XCUIShellViewToggleId;
TEST(XCUIShellChromeStateTest, DefaultsMatchCurrentShellChromeConfiguration) {
XCUIShellChromeState state = {};
const auto& viewToggles = state.GetViewToggles();
EXPECT_FALSE(viewToggles.imguiDemoWindowVisible);
EXPECT_TRUE(viewToggles.nativeBackdropVisible);
EXPECT_TRUE(viewToggles.pulseAccentEnabled);
EXPECT_TRUE(viewToggles.nativeXCUIOverlayVisible);
EXPECT_TRUE(viewToggles.hostedPreviewHudVisible);
const auto* demoPanel = state.TryGetPanelState(XCUIShellPanelId::XCUIDemo);
ASSERT_NE(demoPanel, nullptr);
EXPECT_EQ(demoPanel->panelTitle, "XCUI Demo");
EXPECT_EQ(demoPanel->previewDebugName, "XCUI Demo");
EXPECT_EQ(demoPanel->previewDebugSource, "new_editor.panels.xcui_demo");
EXPECT_TRUE(demoPanel->visible);
EXPECT_TRUE(demoPanel->hostedPreviewEnabled);
EXPECT_EQ(demoPanel->previewMode, XCUIShellHostedPreviewMode::NativeOffscreen);
EXPECT_EQ(
state.GetHostedPreviewState(XCUIShellPanelId::XCUIDemo),
XCUIShellHostedPreviewState::NativeOffscreen);
EXPECT_TRUE(state.IsNativeHostedPreviewActive(XCUIShellPanelId::XCUIDemo));
EXPECT_FALSE(state.IsLegacyHostedPreviewActive(XCUIShellPanelId::XCUIDemo));
const auto* layoutLabPanel = state.TryGetPanelState(XCUIShellPanelId::XCUILayoutLab);
ASSERT_NE(layoutLabPanel, nullptr);
EXPECT_EQ(layoutLabPanel->panelTitle, "XCUI Layout Lab");
EXPECT_EQ(layoutLabPanel->previewDebugName, "XCUI Layout Lab");
EXPECT_EQ(layoutLabPanel->previewDebugSource, "new_editor.panels.xcui_layout_lab");
EXPECT_TRUE(layoutLabPanel->visible);
EXPECT_TRUE(layoutLabPanel->hostedPreviewEnabled);
EXPECT_EQ(layoutLabPanel->previewMode, XCUIShellHostedPreviewMode::LegacyImGui);
EXPECT_EQ(
state.GetHostedPreviewState(XCUIShellPanelId::XCUILayoutLab),
XCUIShellHostedPreviewState::LegacyImGui);
EXPECT_FALSE(state.IsNativeHostedPreviewActive(XCUIShellPanelId::XCUILayoutLab));
EXPECT_TRUE(state.IsLegacyHostedPreviewActive(XCUIShellPanelId::XCUILayoutLab));
}
TEST(XCUIShellChromeStateTest, PanelVisibilityAndPreviewModeMutatorsTrackStateChanges) {
XCUIShellChromeState state = {};
EXPECT_TRUE(state.SetPanelVisible(XCUIShellPanelId::XCUIDemo, false));
EXPECT_FALSE(state.IsPanelVisible(XCUIShellPanelId::XCUIDemo));
EXPECT_FALSE(state.SetPanelVisible(XCUIShellPanelId::XCUIDemo, false));
EXPECT_TRUE(state.TogglePanelVisible(XCUIShellPanelId::XCUIDemo));
EXPECT_TRUE(state.IsPanelVisible(XCUIShellPanelId::XCUIDemo));
EXPECT_TRUE(state.SetHostedPreviewEnabled(XCUIShellPanelId::XCUILayoutLab, false));
EXPECT_FALSE(state.IsHostedPreviewEnabled(XCUIShellPanelId::XCUILayoutLab));
EXPECT_FALSE(state.SetHostedPreviewEnabled(XCUIShellPanelId::XCUILayoutLab, false));
EXPECT_EQ(
state.GetHostedPreviewMode(XCUIShellPanelId::XCUILayoutLab),
XCUIShellHostedPreviewMode::LegacyImGui);
EXPECT_TRUE(state.ToggleHostedPreviewMode(XCUIShellPanelId::XCUILayoutLab));
EXPECT_EQ(
state.GetHostedPreviewMode(XCUIShellPanelId::XCUILayoutLab),
XCUIShellHostedPreviewMode::NativeOffscreen);
EXPECT_TRUE(state.SetHostedPreviewMode(
XCUIShellPanelId::XCUILayoutLab,
XCUIShellHostedPreviewMode::LegacyImGui));
EXPECT_EQ(
state.GetHostedPreviewMode(XCUIShellPanelId::XCUILayoutLab),
XCUIShellHostedPreviewMode::LegacyImGui);
EXPECT_FALSE(state.SetHostedPreviewMode(
XCUIShellPanelId::XCUILayoutLab,
XCUIShellHostedPreviewMode::LegacyImGui));
}
TEST(XCUIShellChromeStateTest, HostedPreviewStateSeparatesEnablementFromRequestedMode) {
XCUIShellChromeState state = {};
EXPECT_EQ(
state.GetHostedPreviewState(XCUIShellPanelId::XCUIDemo),
XCUIShellHostedPreviewState::NativeOffscreen);
EXPECT_TRUE(state.SetHostedPreviewEnabled(XCUIShellPanelId::XCUIDemo, false));
EXPECT_EQ(
state.GetHostedPreviewState(XCUIShellPanelId::XCUIDemo),
XCUIShellHostedPreviewState::Disabled);
EXPECT_FALSE(state.IsNativeHostedPreviewActive(XCUIShellPanelId::XCUIDemo));
EXPECT_FALSE(state.IsLegacyHostedPreviewActive(XCUIShellPanelId::XCUIDemo));
EXPECT_TRUE(state.SetHostedPreviewMode(
XCUIShellPanelId::XCUIDemo,
XCUIShellHostedPreviewMode::LegacyImGui));
EXPECT_EQ(
state.GetHostedPreviewMode(XCUIShellPanelId::XCUIDemo),
XCUIShellHostedPreviewMode::LegacyImGui);
EXPECT_EQ(
state.GetHostedPreviewState(XCUIShellPanelId::XCUIDemo),
XCUIShellHostedPreviewState::Disabled);
EXPECT_TRUE(state.SetHostedPreviewEnabled(XCUIShellPanelId::XCUIDemo, true));
EXPECT_EQ(
state.GetHostedPreviewState(XCUIShellPanelId::XCUIDemo),
XCUIShellHostedPreviewState::LegacyImGui);
EXPECT_FALSE(state.IsNativeHostedPreviewActive(XCUIShellPanelId::XCUIDemo));
EXPECT_TRUE(state.IsLegacyHostedPreviewActive(XCUIShellPanelId::XCUIDemo));
}
TEST(XCUIShellChromeStateTest, ViewToggleMutatorsOnlyFlipRequestedFlags) {
XCUIShellChromeState state = {};
EXPECT_TRUE(state.SetViewToggle(XCUIShellViewToggleId::ImGuiDemoWindow, true));
EXPECT_TRUE(state.GetViewToggle(XCUIShellViewToggleId::ImGuiDemoWindow));
EXPECT_FALSE(state.SetViewToggle(XCUIShellViewToggleId::ImGuiDemoWindow, true));
EXPECT_TRUE(state.ToggleViewToggle(XCUIShellViewToggleId::HostedPreviewHud));
EXPECT_FALSE(state.GetViewToggle(XCUIShellViewToggleId::HostedPreviewHud));
EXPECT_TRUE(state.GetViewToggle(XCUIShellViewToggleId::NativeBackdrop));
EXPECT_TRUE(state.GetViewToggle(XCUIShellViewToggleId::PulseAccent));
EXPECT_TRUE(state.GetViewToggle(XCUIShellViewToggleId::NativeXCUIOverlay));
}
TEST(XCUIShellChromeStateTest, CommandInterfaceTogglesShellViewAndPreviewStates) {
XCUIShellChromeState state = {};
EXPECT_TRUE(state.HasCommand(XCUIShellChromeCommandIds::ToggleXCUIDemoPanel));
EXPECT_TRUE(state.HasCommand(XCUIShellChromeCommandIds::ToggleHostedPreviewHud));
EXPECT_TRUE(state.HasCommand(XCUIShellChromeCommandIds::ToggleNativeLayoutLabPreview));
EXPECT_FALSE(state.HasCommand("new_editor.view.unknown"));
EXPECT_TRUE(state.InvokeCommand(XCUIShellChromeCommandIds::ToggleXCUIDemoPanel));
EXPECT_FALSE(state.IsPanelVisible(XCUIShellPanelId::XCUIDemo));
EXPECT_TRUE(state.InvokeCommand(XCUIShellChromeCommandIds::ToggleHostedPreviewHud));
EXPECT_FALSE(state.GetViewToggle(XCUIShellViewToggleId::HostedPreviewHud));
EXPECT_TRUE(state.InvokeCommand(XCUIShellChromeCommandIds::ToggleNativeLayoutLabPreview));
EXPECT_EQ(
state.GetHostedPreviewMode(XCUIShellPanelId::XCUILayoutLab),
XCUIShellHostedPreviewMode::NativeOffscreen);
EXPECT_TRUE(state.InvokeCommand(XCUIShellChromeCommandIds::ToggleNativeLayoutLabPreview));
EXPECT_EQ(
state.GetHostedPreviewMode(XCUIShellPanelId::XCUILayoutLab),
XCUIShellHostedPreviewMode::LegacyImGui);
EXPECT_FALSE(state.InvokeCommand("new_editor.view.unknown"));
}
TEST(XCUIShellChromeStateTest, PanelCommandIdHelpersMatchCurrentShellCommands) {
EXPECT_EQ(
XCUIShellChromeState::GetPanelVisibilityCommandId(XCUIShellPanelId::XCUIDemo),
XCUIShellChromeCommandIds::ToggleXCUIDemoPanel);
EXPECT_EQ(
XCUIShellChromeState::GetPanelVisibilityCommandId(XCUIShellPanelId::XCUILayoutLab),
XCUIShellChromeCommandIds::ToggleXCUILayoutLabPanel);
EXPECT_EQ(
XCUIShellChromeState::GetPanelPreviewModeCommandId(XCUIShellPanelId::XCUIDemo),
XCUIShellChromeCommandIds::ToggleNativeDemoPanelPreview);
EXPECT_EQ(
XCUIShellChromeState::GetPanelPreviewModeCommandId(XCUIShellPanelId::XCUILayoutLab),
XCUIShellChromeCommandIds::ToggleNativeLayoutLabPreview);
}
TEST(XCUIShellChromeStateTest, ViewToggleCommandIdHelpersMatchCurrentShellCommands) {
EXPECT_EQ(
XCUIShellChromeState::GetViewToggleCommandId(XCUIShellViewToggleId::ImGuiDemoWindow),
XCUIShellChromeCommandIds::ToggleImGuiDemoWindow);
EXPECT_EQ(
XCUIShellChromeState::GetViewToggleCommandId(XCUIShellViewToggleId::NativeBackdrop),
XCUIShellChromeCommandIds::ToggleNativeBackdrop);
EXPECT_EQ(
XCUIShellChromeState::GetViewToggleCommandId(XCUIShellViewToggleId::PulseAccent),
XCUIShellChromeCommandIds::TogglePulseAccent);
EXPECT_EQ(
XCUIShellChromeState::GetViewToggleCommandId(XCUIShellViewToggleId::NativeXCUIOverlay),
XCUIShellChromeCommandIds::ToggleNativeXCUIOverlay);
EXPECT_EQ(
XCUIShellChromeState::GetViewToggleCommandId(XCUIShellViewToggleId::HostedPreviewHud),
XCUIShellChromeCommandIds::ToggleHostedPreviewHud);
}
TEST(XCUIShellChromeStateTest, InvalidPanelAndToggleIdsFailGracefully) {
XCUIShellChromeState state = {};
const XCUIShellPanelId invalidPanelId = static_cast<XCUIShellPanelId>(255);
const XCUIShellViewToggleId invalidToggleId = static_cast<XCUIShellViewToggleId>(255);
EXPECT_EQ(state.TryGetPanelState(invalidPanelId), nullptr);
EXPECT_FALSE(state.IsPanelVisible(invalidPanelId));
EXPECT_FALSE(state.SetPanelVisible(invalidPanelId, true));
EXPECT_FALSE(state.TogglePanelVisible(invalidPanelId));
EXPECT_FALSE(state.IsHostedPreviewEnabled(invalidPanelId));
EXPECT_FALSE(state.SetHostedPreviewEnabled(invalidPanelId, false));
EXPECT_EQ(
state.GetHostedPreviewMode(invalidPanelId),
XCUIShellHostedPreviewMode::LegacyImGui);
EXPECT_EQ(
state.GetHostedPreviewState(invalidPanelId),
XCUIShellHostedPreviewState::Disabled);
EXPECT_FALSE(state.IsNativeHostedPreviewActive(invalidPanelId));
EXPECT_FALSE(state.IsLegacyHostedPreviewActive(invalidPanelId));
EXPECT_FALSE(state.SetHostedPreviewMode(invalidPanelId, XCUIShellHostedPreviewMode::NativeOffscreen));
EXPECT_FALSE(state.ToggleHostedPreviewMode(invalidPanelId));
EXPECT_FALSE(state.GetViewToggle(invalidToggleId));
EXPECT_FALSE(state.SetViewToggle(invalidToggleId, true));
EXPECT_FALSE(state.ToggleViewToggle(invalidToggleId));
EXPECT_TRUE(XCUIShellChromeState::GetPanelVisibilityCommandId(invalidPanelId).empty());
EXPECT_TRUE(XCUIShellChromeState::GetPanelPreviewModeCommandId(invalidPanelId).empty());
EXPECT_TRUE(XCUIShellChromeState::GetViewToggleCommandId(invalidToggleId).empty());
}
} // namespace