From a35adf14d37285b75bb72c40eaad77faf4a4c2a2 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 5 Apr 2026 12:53:05 +0800 Subject: [PATCH] Wire XCUI layout lab panel keyboard navigation --- docs/plan/XCUI_Phase_Status_2026-04-05.md | 5 +- new_editor/src/panels/XCUILayoutLabPanel.cpp | 34 +++ new_editor/src/panels/XCUILayoutLabPanel.h | 2 + .../NewEditor/test_xcui_layout_lab_panel.cpp | 253 ++++++++++++++++++ 4 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 tests/NewEditor/test_xcui_layout_lab_panel.cpp diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index 6604e126..4797570e 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -87,6 +87,7 @@ Current gap: - 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. +- `LayoutLab` panel input now also maps concrete arrow/home/end keys into those shared navigation actions, so keyboard traversal is reachable from the sandbox UI instead of staying runtime-only. - `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`. @@ -96,7 +97,7 @@ Current gap: - The shell is still ImGui-hosted. - 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 panel-level keyboard mapping plus shell-state adoption 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 broader editor-host keybinding plus full shell-state adoption are still only partially integrated. ## Validated This Phase @@ -111,6 +112,7 @@ Current gap: - `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` +- `new_editor_xcui_layout_lab_panel_tests`: `2/2` - `XCNewEditor` Debug target builds successfully - `core_ui_tests`: `52 total` (`50` passed, `2` skipped because `KeyCode::Delete` currently aliases `Backspace`) - `scene_tests`: `68/68` @@ -214,6 +216,7 @@ Current gap: - 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. +- `LayoutLab` panel now maps concrete arrow/home/end keys into the shared navigation model, with dedicated panel-level coverage proving that the sandbox UI can actually drive the runtime navigation seam end-to-end. - `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. diff --git a/new_editor/src/panels/XCUILayoutLabPanel.cpp b/new_editor/src/panels/XCUILayoutLabPanel.cpp index 2d9c33e3..849bf7a3 100644 --- a/new_editor/src/panels/XCUILayoutLabPanel.cpp +++ b/new_editor/src/panels/XCUILayoutLabPanel.cpp @@ -46,6 +46,33 @@ const char* GetPreviewStateLabel( return previewStats.presented ? "live" : "idle"; } +bool ShouldCaptureKeyboardNavigation( + const ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession& canvasSession, + const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& previousFrame) { + if (!canvasSession.validCanvas) { + return false; + } + + return canvasSession.hovered || + (canvasSession.windowFocused && + !previousFrame.stats.selectedElementId.empty()); +} + +void PopulateKeyboardNavigationInput( + ::XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState& input, + bool captureKeyboardNavigation) { + if (!captureKeyboardNavigation) { + return; + } + + input.navigatePrevious = ImGui::IsKeyPressed(ImGuiKey_UpArrow); + input.navigateNext = ImGui::IsKeyPressed(ImGuiKey_DownArrow); + input.navigateHome = ImGui::IsKeyPressed(ImGuiKey_Home); + input.navigateEnd = ImGui::IsKeyPressed(ImGuiKey_End); + input.navigateCollapse = ImGui::IsKeyPressed(ImGuiKey_LeftArrow); + input.navigateExpand = ImGui::IsKeyPressed(ImGuiKey_RightArrow); +} + } // namespace XCUILayoutLabPanel::XCUILayoutLabPanel(::XCEngine::Editor::XCUIBackend::XCUIWin32InputSource* inputSource) @@ -79,6 +106,10 @@ const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& XCUILayoutLabPane return m_lastPreviewStats; } +bool XCUILayoutLabPanel::TryGetElementRect(const std::string& elementId, ::XCEngine::UI::UIRect& outRect) const { + return m_runtime.TryGetElementRect(elementId, outRect); +} + void XCUILayoutLabPanel::SetHostedPreviewPresenter( std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter> previewPresenter) { m_previewPresenter = std::move(previewPresenter); @@ -168,6 +199,9 @@ void XCUILayoutLabPanel::Render() { input.pointerInside = validCanvas && canvasSession.hovered; } input.pointerPressed = input.pointerInside && ImGui::IsMouseClicked(ImGuiMouseButton_Left); + PopulateKeyboardNavigationInput( + input, + ShouldCaptureKeyboardNavigation(canvasSession, m_runtime.GetFrameResult())); const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& frame = m_runtime.Update(input); diff --git a/new_editor/src/panels/XCUILayoutLabPanel.h b/new_editor/src/panels/XCUILayoutLabPanel.h index 152d0279..a7cb650b 100644 --- a/new_editor/src/panels/XCUILayoutLabPanel.h +++ b/new_editor/src/panels/XCUILayoutLabPanel.h @@ -8,6 +8,7 @@ #include "XCUIBackend/XCUILayoutLabRuntime.h" #include +#include namespace XCEngine { namespace NewEditor { @@ -31,6 +32,7 @@ public: bool IsUsingNativeHostedPreview() const; const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& GetFrameResult() const; const ::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats& GetLastPreviewStats() const; + bool TryGetElementRect(const std::string& elementId, ::XCEngine::UI::UIRect& outRect) const; private: bool m_lastReloadSucceeded = false; diff --git a/tests/NewEditor/test_xcui_layout_lab_panel.cpp b/tests/NewEditor/test_xcui_layout_lab_panel.cpp new file mode 100644 index 00000000..bd967630 --- /dev/null +++ b/tests/NewEditor/test_xcui_layout_lab_panel.cpp @@ -0,0 +1,253 @@ +#include + +#include "panels/XCUILayoutLabPanel.h" + +#include "XCUIBackend/XCUIHostedPreviewPresenter.h" +#include "XCUIBackend/XCUIPanelCanvasHost.h" + +#include + +#include +#include +#include + +namespace { + +using XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter; +using XCEngine::Editor::XCUIBackend::IXCUIPanelCanvasHost; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame; +using XCEngine::Editor::XCUIBackend::XCUIHostedPreviewStats; +using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasRequest; +using XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession; +using XCEngine::NewEditor::XCUILayoutLabPanel; + +class ImGuiContextScope { +public: + ImGuiContextScope() { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + } + + ~ImGuiContextScope() { + ImGui::DestroyContext(); + } +}; + +void PrepareImGui(float width = 1280.0f, float height = 900.0f) { + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(width, height); + io.DeltaTime = 1.0f / 60.0f; + unsigned char* fontPixels = nullptr; + int fontWidth = 0; + int fontHeight = 0; + io.Fonts->GetTexDataAsRGBA32(&fontPixels, &fontWidth, &fontHeight); + io.Fonts->SetTexID(static_cast(1)); +} + +class StubHostedPreviewPresenter final : public IXCUIHostedPreviewPresenter { +public: + bool Present(const XCUIHostedPreviewFrame& frame) override { + m_lastStats = {}; + m_lastStats.presented = frame.drawData != nullptr; + return m_lastStats.presented; + } + + const XCUIHostedPreviewStats& GetLastStats() const override { + return m_lastStats; + } + +private: + XCUIHostedPreviewStats m_lastStats = {}; +}; + +class StubCanvasHost final : public IXCUIPanelCanvasHost { +public: + const char* GetDebugName() const override { + return "StubCanvasHost"; + } + + XCEngine::Editor::XCUIBackend::XCUIPanelCanvasHostBackend GetBackend() const override { + return XCEngine::Editor::XCUIBackend::XCUIPanelCanvasHostBackend::Null; + } + + XCEngine::Editor::XCUIBackend::XCUIPanelCanvasHostCapabilities GetCapabilities() const override { + return {}; + } + + XCUIPanelCanvasSession BeginCanvas(const XCUIPanelCanvasRequest&) override { + return m_session; + } + + void DrawFilledRect( + const XCEngine::UI::UIRect&, + const XCEngine::UI::UIColor&, + float) override { + } + + void DrawOutlineRect( + const XCEngine::UI::UIRect&, + const XCEngine::UI::UIColor&, + float, + float) override { + } + + void DrawText( + const XCEngine::UI::UIPoint&, + std::string_view, + const XCEngine::UI::UIColor&, + float) override { + } + + void EndCanvas() override { + } + + void SetPointerPosition(const XCEngine::UI::UIPoint& position) { + m_session.pointerPosition = position; + } + + void SetHovered(bool hovered) { + m_session.hovered = hovered; + } + + void SetWindowFocused(bool focused) { + m_session.windowFocused = focused; + } + +private: + XCUIPanelCanvasSession m_session = { + XCEngine::UI::UIRect(0.0f, 0.0f, 960.0f, 640.0f), + XCEngine::UI::UIRect(0.0f, 0.0f, 960.0f, 640.0f), + XCEngine::UI::UIPoint(120.0f, 120.0f), + true, + true, + true + }; +}; + +void RenderPanelFrame( + XCUILayoutLabPanel& panel, + ImGuiContextScope&, + const std::function& configureIo = nullptr) { + PrepareImGui(); + if (configureIo) { + configureIo(ImGui::GetIO()); + } + + ImGui::NewFrame(); + panel.Render(); + ImGui::Render(); +} + +void ClickElement( + XCUILayoutLabPanel& panel, + StubCanvasHost& canvasHost, + const std::string& elementId, + ImGuiContextScope& contextScope) { + XCEngine::UI::UIRect elementRect = {}; + ASSERT_TRUE(panel.TryGetElementRect(elementId, elementRect)); + const XCEngine::UI::UIPoint clickPoint( + elementRect.x + elementRect.width * 0.5f, + elementRect.y + elementRect.height * 0.5f); + + canvasHost.SetPointerPosition(clickPoint); + canvasHost.SetHovered(true); + RenderPanelFrame( + panel, + contextScope, + [](ImGuiIO& io) { + io.AddMouseButtonEvent(ImGuiMouseButton_Left, true); + }); + RenderPanelFrame( + panel, + contextScope, + [](ImGuiIO& io) { + io.AddMouseButtonEvent(ImGuiMouseButton_Left, false); + }); +} + +void PressKeyOnce( + XCUILayoutLabPanel& panel, + ImGuiContextScope& contextScope, + ImGuiKey key) { + RenderPanelFrame( + panel, + contextScope, + [key](ImGuiIO& io) { + io.AddKeyEvent(key, true); + }); + RenderPanelFrame( + panel, + contextScope, + [key](ImGuiIO& io) { + io.AddKeyEvent(key, false); + }); +} + +TEST(NewEditorXCUILayoutLabPanelTest, MapsPreviousNextHomeAndEndIntoRuntimeNavigation) { + ImGuiContextScope contextScope; + + auto previewPresenter = std::make_unique(); + auto canvasHost = std::make_unique(); + StubCanvasHost* canvasHostPtr = canvasHost.get(); + + XCUILayoutLabPanel panel(nullptr, std::move(previewPresenter)); + panel.SetHostedPreviewEnabled(false); + panel.SetCanvasHost(std::move(canvasHost)); + + RenderPanelFrame(panel, contextScope); + ClickElement(panel, *canvasHostPtr, "assetLighting", contextScope); + ASSERT_EQ(panel.GetFrameResult().stats.selectedElementId, "assetLighting"); + + canvasHostPtr->SetHovered(false); + canvasHostPtr->SetWindowFocused(true); + + PressKeyOnce(panel, contextScope, ImGuiKey_DownArrow); + EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "assetMaterials"); + + PressKeyOnce(panel, contextScope, ImGuiKey_UpArrow); + EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "assetLighting"); + + PressKeyOnce(panel, contextScope, ImGuiKey_End); + const std::string endSelection = panel.GetFrameResult().stats.selectedElementId; + ASSERT_FALSE(endSelection.empty()); + EXPECT_NE(endSelection, "assetLighting"); + + PressKeyOnce(panel, contextScope, ImGuiKey_Home); + EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "assetLighting"); +} + +TEST(NewEditorXCUILayoutLabPanelTest, MapsCollapseAndExpandIntoRuntimeNavigation) { + ImGuiContextScope contextScope; + + auto previewPresenter = std::make_unique(); + auto canvasHost = std::make_unique(); + StubCanvasHost* canvasHostPtr = canvasHost.get(); + + XCUILayoutLabPanel panel(nullptr, std::move(previewPresenter)); + panel.SetHostedPreviewEnabled(false); + panel.SetCanvasHost(std::move(canvasHost)); + + RenderPanelFrame(panel, contextScope); + ClickElement(panel, *canvasHostPtr, "treeScenes", contextScope); + ASSERT_EQ(panel.GetFrameResult().stats.selectedElementId, "treeScenes"); + + canvasHostPtr->SetHovered(false); + canvasHostPtr->SetWindowFocused(true); + + PressKeyOnce(panel, contextScope, ImGuiKey_LeftArrow); + EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "treeAssetsRoot"); + + PressKeyOnce(panel, contextScope, ImGuiKey_LeftArrow); + EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "treeAssetsRoot"); + EXPECT_EQ(panel.GetFrameResult().stats.expandedTreeItemCount, 0u); + + PressKeyOnce(panel, contextScope, ImGuiKey_RightArrow); + EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "treeAssetsRoot"); + EXPECT_EQ(panel.GetFrameResult().stats.expandedTreeItemCount, 1u); + + PressKeyOnce(panel, contextScope, ImGuiKey_RightArrow); + EXPECT_EQ(panel.GetFrameResult().stats.selectedElementId, "treeScenes"); +} + +} // namespace