From e5e9f348a36613c046c66631944c08bef6f367a0 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 5 Apr 2026 12:50:55 +0800 Subject: [PATCH] Integrate XCUI shell state and runtime frame seams --- docs/plan/XCUI_Phase_Status_2026-04-05.md | 44 +- .../UI/Runtime/UISceneRuntimeContext.h | 1 + .../XCEngine/UI/Runtime/UIScreenPlayer.h | 1 + .../XCEngine/UI/Runtime/UIScreenTypes.h | 4 + engine/include/XCEngine/UI/Runtime/UISystem.h | 1 + .../src/UI/Runtime/UISceneRuntimeContext.cpp | 4 + engine/src/UI/Runtime/UIScreenPlayer.cpp | 6 + engine/src/UI/Runtime/UISystem.cpp | 12 + new_editor/src/Application.cpp | 355 +++++++++--- new_editor/src/Application.h | 269 ++++++++- .../ImGuiXCUIHostedPreviewPresenter.h | 46 +- .../XCUIBackend/ImGuiXCUIPanelCanvasHost.h | 16 + .../src/XCUIBackend/NullXCUIPanelCanvasHost.h | 71 +++ .../src/XCUIBackend/XCUIDemoRuntime.cpp | 54 +- .../XCUIBackend/XCUIHostedPreviewPresenter.h | 2 - .../src/XCUIBackend/XCUILayoutLabRuntime.cpp | 510 ++++++++++++++++++ .../src/XCUIBackend/XCUILayoutLabRuntime.h | 6 + .../src/XCUIBackend/XCUIPanelCanvasHost.h | 17 +- .../src/XCUIBackend/XCUIShellChromeState.cpp | 295 ++++++++++ .../src/XCUIBackend/XCUIShellChromeState.h | 109 ++++ tests/Core/UI/test_ui_runtime.cpp | 333 ++++++++++++ tests/NewEditor/CMakeLists.txt | 181 +++++++ ...est_application_shell_command_bindings.cpp | 250 +++++++++ .../test_imgui_xcui_panel_canvas_host.cpp | 25 + tests/NewEditor/test_xcui_demo_runtime.cpp | 67 +++ .../test_xcui_hosted_preview_presenter.cpp | 151 +++++- .../test_xcui_layout_lab_runtime.cpp | 167 ++++++ .../NewEditor/test_xcui_panel_canvas_host.cpp | 67 +++ .../test_xcui_shell_chrome_state.cpp | 221 ++++++++ 29 files changed, 3183 insertions(+), 102 deletions(-) create mode 100644 new_editor/src/XCUIBackend/NullXCUIPanelCanvasHost.h create mode 100644 new_editor/src/XCUIBackend/XCUIShellChromeState.cpp create mode 100644 new_editor/src/XCUIBackend/XCUIShellChromeState.h create mode 100644 tests/NewEditor/test_application_shell_command_bindings.cpp create mode 100644 tests/NewEditor/test_imgui_xcui_panel_canvas_host.cpp create mode 100644 tests/NewEditor/test_xcui_panel_canvas_host.cpp create mode 100644 tests/NewEditor/test_xcui_shell_chrome_state.cpp diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index 27f261c1..6604e126 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -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. diff --git a/engine/include/XCEngine/UI/Runtime/UISceneRuntimeContext.h b/engine/include/XCEngine/UI/Runtime/UISceneRuntimeContext.h index f81499ca..87eadeb4 100644 --- a/engine/include/XCEngine/UI/Runtime/UISceneRuntimeContext.h +++ b/engine/include/XCEngine/UI/Runtime/UISceneRuntimeContext.h @@ -19,6 +19,7 @@ public: const UIScreenStackController& GetStackController() const; const UISystemFrameResult& GetLastFrame() const; + UISystemFrameResult ConsumeLastFrame(); void Reset(); void SetViewportRect(const UIRect& viewportRect); diff --git a/engine/include/XCEngine/UI/Runtime/UIScreenPlayer.h b/engine/include/XCEngine/UI/Runtime/UIScreenPlayer.h index 0889d868..c52ef7a1 100644 --- a/engine/include/XCEngine/UI/Runtime/UIScreenPlayer.h +++ b/engine/include/XCEngine/UI/Runtime/UIScreenPlayer.h @@ -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; diff --git a/engine/include/XCEngine/UI/Runtime/UIScreenTypes.h b/engine/include/XCEngine/UI/Runtime/UIScreenTypes.h index f5fcb65f..b1bc4d59 100644 --- a/engine/include/XCEngine/UI/Runtime/UIScreenTypes.h +++ b/engine/include/XCEngine/UI/Runtime/UIScreenTypes.h @@ -89,9 +89,13 @@ struct UISystemPresentedLayer { struct UISystemFrameResult { UIDrawData drawData = {}; std::vector 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 = {}; }; diff --git a/engine/include/XCEngine/UI/Runtime/UISystem.h b/engine/include/XCEngine/UI/Runtime/UISystem.h index 07fb4307..0be18d35 100644 --- a/engine/include/XCEngine/UI/Runtime/UISystem.h +++ b/engine/include/XCEngine/UI/Runtime/UISystem.h @@ -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>& GetPlayers() const; diff --git a/engine/src/UI/Runtime/UISceneRuntimeContext.cpp b/engine/src/UI/Runtime/UISceneRuntimeContext.cpp index 43765903..701b51f3 100644 --- a/engine/src/UI/Runtime/UISceneRuntimeContext.cpp +++ b/engine/src/UI/Runtime/UISceneRuntimeContext.cpp @@ -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(); diff --git a/engine/src/UI/Runtime/UIScreenPlayer.cpp b/engine/src/UI/Runtime/UIScreenPlayer.cpp index 7a182061..809e3553 100644 --- a/engine/src/UI/Runtime/UIScreenPlayer.cpp +++ b/engine/src/UI/Runtime/UIScreenPlayer.cpp @@ -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; } diff --git a/engine/src/UI/Runtime/UISystem.cpp b/engine/src/UI/Runtime/UISystem.cpp index 23f81e5b..7414d361 100644 --- a/engine/src/UI/Runtime/UISystem.cpp +++ b/engine/src/UI/Runtime/UISystem.cpp @@ -1,5 +1,7 @@ #include +#include + 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>& UISystem::GetPlayers() const { return m_players; } diff --git a/new_editor/src/Application.cpp b/new_editor/src/Application.cpp index d379b5ab..b212ad25 100644 --- a/new_editor/src/Application.cpp +++ b/new_editor/src/Application.cpp @@ -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( &m_xcuiInputSource, - CreateHostedPreviewPresenter(m_showNativeDemoPanelPreview)); + CreateHostedPreviewPresenter(IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo))); m_layoutLabPanel = std::make_unique( &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( std::chrono::duration(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(surface.GetWidth()); const float height = static_cast(surface.GetHeight()); const float horizontalMargin = (std::min)(width * 0.14f, 128.0f); diff --git a/new_editor/src/Application.h b/new_editor/src/Application.h index 62db8bdd..5bf1613d 100644 --- a/new_editor/src/Application.h +++ b/new_editor/src/Application.h @@ -1,5 +1,8 @@ #pragma once +#include + +#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 #include #include +#include +#include #include #include #include @@ -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 getXCUIDemoPanelVisible = {}; + std::function setXCUIDemoPanelVisible = {}; + std::function getXCUILayoutLabPanelVisible = {}; + std::function setXCUILayoutLabPanelVisible = {}; + std::function getImGuiDemoWindowVisible = {}; + std::function setImGuiDemoWindowVisible = {}; + std::function getNativeBackdropVisible = {}; + std::function setNativeBackdropVisible = {}; + std::function getPulseAccentEnabled = {}; + std::function setPulseAccentEnabled = {}; + std::function getNativeXCUIOverlayVisible = {}; + std::function setNativeXCUIOverlayVisible = {}; + std::function getHostedPreviewHudVisible = {}; + std::function setHostedPreviewHudVisible = {}; + std::function getNativeDemoPanelPreviewEnabled = {}; + std::function setNativeDemoPanelPreviewEnabled = {}; + std::function getNativeLayoutLabPreviewEnabled = {}; + std::function setNativeLayoutLabPreviewEnabled = {}; + std::function 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& getter, + const std::function& setter, + std::initializer_list accelerators, + const std::function& afterToggle = {}) { + if (!getter || !setter) { + return; + } + + XCUIEditorCommandDefinition definition = {}; + definition.commandId = commandId; + definition.isEnabled = [getter, setter]() { + return static_cast(getter) && static_cast(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(KeyCode::One), + ctrlOnly, + true, + false } }); + bindToggleCommand( + ShellCommandIds::ToggleXCUILayoutLabPanel, + bindings.getXCUILayoutLabPanelVisible, + bindings.setXCUILayoutLabPanelVisible, + { XCUIEditorCommandAccelerator{ + static_cast(KeyCode::Two), + ctrlOnly, + true, + false } }); + bindToggleCommand( + ShellCommandIds::ToggleImGuiDemoWindow, + bindings.getImGuiDemoWindowVisible, + bindings.setImGuiDemoWindowVisible, + { XCUIEditorCommandAccelerator{ + static_cast(KeyCode::Three), + ctrlOnly, + true, + false } }); + bindToggleCommand( + ShellCommandIds::ToggleNativeBackdrop, + bindings.getNativeBackdropVisible, + bindings.setNativeBackdropVisible, + { XCUIEditorCommandAccelerator{ + static_cast(KeyCode::B), + ctrlShift, + true, + false } }); + bindToggleCommand( + ShellCommandIds::TogglePulseAccent, + bindings.getPulseAccentEnabled, + bindings.setPulseAccentEnabled, + { XCUIEditorCommandAccelerator{ + static_cast(KeyCode::P), + ctrlShift, + true, + false } }); + bindToggleCommand( + ShellCommandIds::ToggleNativeXCUIOverlay, + bindings.getNativeXCUIOverlayVisible, + bindings.setNativeXCUIOverlayVisible, + { XCUIEditorCommandAccelerator{ + static_cast(KeyCode::O), + ctrlShift, + true, + false } }); + bindToggleCommand( + ShellCommandIds::ToggleHostedPreviewHud, + bindings.getHostedPreviewHudVisible, + bindings.setHostedPreviewHudVisible, + { XCUIEditorCommandAccelerator{ + static_cast(KeyCode::H), + ctrlShift, + true, + false } }); + bindToggleCommand( + ShellCommandIds::ToggleNativeDemoPanelPreview, + bindings.getNativeDemoPanelPreviewEnabled, + bindings.setNativeDemoPanelPreviewEnabled, + { XCUIEditorCommandAccelerator{ + static_cast(KeyCode::One), + ctrlAlt, + true, + false } }, + bindings.onHostedPreviewModeChanged); + bindToggleCommand( + ShellCommandIds::ToggleNativeLayoutLabPreview, + bindings.getNativeLayoutLabPreviewEnabled, + bindings.setNativeLayoutLabPreviewEnabled, + { XCUIEditorCommandAccelerator{ + static_cast(KeyCode::Two), + ctrlAlt, + true, + false } }, + bindings.onHostedPreviewModeChanged); + } + int Run(HINSTANCE instance, int nCmdShow); private: + using ShellPanelStateArray = std::array(ShellPanelId::Count)>; + + static constexpr std::size_t GetShellPanelIndex(ShellPanelId panelId) { + return static_cast(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 m_demoPanel; std::unique_ptr 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 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 = {}; diff --git a/new_editor/src/XCUIBackend/ImGuiXCUIHostedPreviewPresenter.h b/new_editor/src/XCUIBackend/ImGuiXCUIHostedPreviewPresenter.h index 7a6faa05..8f361a4b 100644 --- a/new_editor/src/XCUIBackend/ImGuiXCUIHostedPreviewPresenter.h +++ b/new_editor/src/XCUIBackend/ImGuiXCUIHostedPreviewPresenter.h @@ -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 targetBinding = {}) + : m_targetBinding(std::move(targetBinding)) { + if (m_targetBinding == nullptr) { + m_targetBinding = std::make_unique(); + } + } + 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 m_targetBinding = {}; XCUIHostedPreviewStats m_lastStats = {}; }; +inline std::unique_ptr +CreateImGuiCurrentWindowXCUIHostedPreviewTargetBinding() { + return std::make_unique(); +} + +inline std::unique_ptr CreateImGuiXCUIHostedPreviewPresenter( + std::unique_ptr targetBinding) { + return std::make_unique(std::move(targetBinding)); +} + inline std::unique_ptr CreateImGuiXCUIHostedPreviewPresenter() { - return std::make_unique(); + return CreateImGuiXCUIHostedPreviewPresenter(CreateImGuiCurrentWindowXCUIHostedPreviewTargetBinding()); } } // namespace XCUIBackend diff --git a/new_editor/src/XCUIBackend/ImGuiXCUIPanelCanvasHost.h b/new_editor/src/XCUIBackend/ImGuiXCUIPanelCanvasHost.h index 7a3532f0..db1db004 100644 --- a/new_editor/src/XCUIBackend/ImGuiXCUIPanelCanvasHost.h +++ b/new_editor/src/XCUIBackend/ImGuiXCUIPanelCanvasHost.h @@ -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' diff --git a/new_editor/src/XCUIBackend/NullXCUIPanelCanvasHost.h b/new_editor/src/XCUIBackend/NullXCUIPanelCanvasHost.h new file mode 100644 index 00000000..a9697b1b --- /dev/null +++ b/new_editor/src/XCUIBackend/NullXCUIPanelCanvasHost.h @@ -0,0 +1,71 @@ +#pragma once + +#include "XCUIBackend/XCUIPanelCanvasHost.h" + +#include + +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 CreateNullXCUIPanelCanvasHost() { + return std::make_unique(); +} + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/new_editor/src/XCUIBackend/XCUIDemoRuntime.cpp b/new_editor/src/XCUIBackend/XCUIDemoRuntime.cpp index 83bae540..d8f5c7e4 100644 --- a/new_editor/src/XCUIBackend/XCUIDemoRuntime.cpp +++ b/new_editor/src/XCUIBackend/XCUIDemoRuntime.cpp @@ -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 = diff --git a/new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h b/new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h index 0cce20a2..79708d2c 100644 --- a/new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h +++ b/new_editor/src/XCUIBackend/XCUIHostedPreviewPresenter.h @@ -300,8 +300,6 @@ private: XCUIHostedPreviewStats m_lastStats = {}; }; -std::unique_ptr CreateImGuiXCUIHostedPreviewPresenter(); - inline std::unique_ptr CreateQueuedNativeXCUIHostedPreviewPresenter( XCUIHostedPreviewQueue& queue, XCUIHostedPreviewSurfaceRegistry& surfaceRegistry) { diff --git a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp index 4cac5971..20c6796c 100644 --- a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp +++ b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -80,6 +81,9 @@ struct RuntimeBuildContext { std::unordered_map nodeIndexById = {}; std::unordered_map 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 itemIndices = {}; +}; + String ToContainersString(const std::string& value) { return String(value.c_str()); } @@ -660,6 +669,458 @@ std::vector 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 CollectVisibleChildrenOfKind( + const RuntimeBuildContext& state, + std::size_t nodeIndex, + UIWidgets::UIEditorCollectionPrimitiveKind kind) { + std::vector 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& 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 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 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); } } diff --git a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h index c5284d66..336f29d8 100644 --- a/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h +++ b/new_editor/src/XCUIBackend/XCUILayoutLabRuntime.h @@ -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 { diff --git a/new_editor/src/XCUIBackend/XCUIPanelCanvasHost.h b/new_editor/src/XCUIBackend/XCUIPanelCanvasHost.h index 973123fa..933b161c 100644 --- a/new_editor/src/XCUIBackend/XCUIPanelCanvasHost.h +++ b/new_editor/src/XCUIBackend/XCUIPanelCanvasHost.h @@ -4,6 +4,7 @@ #include +#include #include #include @@ -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 CreateImGuiXCUIPanelCanvasHost(); +std::unique_ptr CreateNullXCUIPanelCanvasHost(); } // namespace XCUIBackend } // namespace Editor diff --git a/new_editor/src/XCUIBackend/XCUIShellChromeState.cpp b/new_editor/src/XCUIBackend/XCUIShellChromeState.cpp new file mode 100644 index 00000000..8eac9848 --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIShellChromeState.cpp @@ -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(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(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 diff --git a/new_editor/src/XCUIBackend/XCUIShellChromeState.h b/new_editor/src/XCUIBackend/XCUIShellChromeState.h new file mode 100644 index 00000000..a369f7dc --- /dev/null +++ b/new_editor/src/XCUIBackend/XCUIShellChromeState.h @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include + +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(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(XCUIShellPanelId::Count)> m_panels = {}; +}; + +} // namespace XCUIBackend +} // namespace Editor +} // namespace XCEngine diff --git a/tests/Core/UI/test_ui_runtime.cpp b/tests/Core/UI/test_ui_runtime.cpp index 8f74bd29..ff85871b 100644 --- a/tests/Core/UI/test_ui_runtime.cpp +++ b/tests/Core/UI/test_ui_runtime.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -9,12 +10,14 @@ #include #include #include +#include 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 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); +} diff --git a/tests/NewEditor/CMakeLists.txt b/tests/NewEditor/CMakeLists.txt index 17e1147d..9d92b123 100644 --- a/tests/NewEditor/CMakeLists.txt +++ b/tests/NewEditor/CMakeLists.txt @@ -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() diff --git a/tests/NewEditor/test_application_shell_command_bindings.cpp b/tests/NewEditor/test_application_shell_command_bindings.cpp new file mode 100644 index 00000000..af8a5b2e --- /dev/null +++ b/tests/NewEditor/test_application_shell_command_bindings.cpp @@ -0,0 +1,250 @@ +#include "Application.h" + +#include + +#include + +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(panelId); +} + +struct ShellCommandHarness { + Application::ShellViewToggleState viewToggles = {}; + std::array(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(KeyCode::One)); + frameDelta.keyboard.repeatedKeys.push_back(static_cast(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(KeyCode::One))); + EXPECT_TRUE(snapshot.IsKeyDown(static_cast(KeyCode::Two))); + + const auto* repeatedKey = snapshot.FindKeyState(static_cast(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(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(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(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(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(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(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 diff --git a/tests/NewEditor/test_imgui_xcui_panel_canvas_host.cpp b/tests/NewEditor/test_imgui_xcui_panel_canvas_host.cpp new file mode 100644 index 00000000..41c95fd5 --- /dev/null +++ b/tests/NewEditor/test_imgui_xcui_panel_canvas_host.cpp @@ -0,0 +1,25 @@ +#include + +#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 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 diff --git a/tests/NewEditor/test_xcui_demo_runtime.cpp b/tests/NewEditor/test_xcui_demo_runtime.cpp index 2d763447..8abb587e 100644 --- a/tests/NewEditor/test_xcui_demo_runtime.cpp +++ b/tests/NewEditor/test_xcui_demo_runtime.cpp @@ -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({ "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({ + "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()); diff --git a/tests/NewEditor/test_xcui_hosted_preview_presenter.cpp b/tests/NewEditor/test_xcui_hosted_preview_presenter.cpp index e8541afb..525d19a3 100644 --- a/tests/NewEditor/test_xcui_hosted_preview_presenter.cpp +++ b/tests/NewEditor/test_xcui_hosted_preview_presenter.cpp @@ -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 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 targetBinding = + std::make_unique(); + RecordingImGuiHostedPreviewTargetBinding* targetBindingPtr = targetBinding.get(); + targetBindingPtr->resolvedDrawList = targetDrawList; + + std::unique_ptr 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 targetBinding = + std::make_unique(); + RecordingImGuiHostedPreviewTargetBinding* targetBindingPtr = targetBinding.get(); + + std::unique_ptr 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 targetBinding = + std::make_unique(); + RecordingImGuiHostedPreviewTargetBinding* targetBindingPtr = targetBinding.get(); + targetBindingPtr->resolvedDrawList = targetDrawList; + + std::unique_ptr 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 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); } diff --git a/tests/NewEditor/test_xcui_layout_lab_runtime.cpp b/tests/NewEditor/test_xcui_layout_lab_runtime.cpp index ceca3a65..825bf04d 100644 --- a/tests/NewEditor/test_xcui_layout_lab_runtime.cpp +++ b/tests/NewEditor/test_xcui_layout_lab_runtime.cpp @@ -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 CollectTextCommands(const XCEngine::UI::UIDrawData& drawData) { std::vector 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"); +} diff --git a/tests/NewEditor/test_xcui_panel_canvas_host.cpp b/tests/NewEditor/test_xcui_panel_canvas_host.cpp new file mode 100644 index 00000000..cfb52c0e --- /dev/null +++ b/tests/NewEditor/test_xcui_panel_canvas_host.cpp @@ -0,0 +1,67 @@ +#include + +#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 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 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 diff --git a/tests/NewEditor/test_xcui_shell_chrome_state.cpp b/tests/NewEditor/test_xcui_shell_chrome_state.cpp new file mode 100644 index 00000000..cb05e119 --- /dev/null +++ b/tests/NewEditor/test_xcui_shell_chrome_state.cpp @@ -0,0 +1,221 @@ +#include "XCUIBackend/XCUIShellChromeState.h" + +#include + +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(255); + const XCUIShellViewToggleId invalidToggleId = static_cast(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