diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index a385381c..afdd04a9 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -20,6 +20,7 @@ add_library(XCUIEditorLib STATIC src/Core/UIEditorPanelRegistry.cpp src/Core/UIEditorShortcutManager.cpp src/Core/UIEditorViewportInputBridge.cpp + src/Core/UIEditorViewportShell.cpp src/Core/UIEditorWorkspaceLayoutPersistence.cpp src/Core/UIEditorWorkspaceController.cpp src/Core/UIEditorWorkspaceModel.cpp diff --git a/new_editor/include/XCEditor/Core/UIEditorViewportShell.h b/new_editor/include/XCEditor/Core/UIEditorViewportShell.h new file mode 100644 index 00000000..441a0b0f --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorViewportShell.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +#include + +namespace XCEngine::UI::Editor { + +struct UIEditorViewportShellVisualState { + std::size_t hoveredToolIndex = Widgets::UIEditorViewportSlotInvalidIndex; + std::size_t activeToolIndex = Widgets::UIEditorViewportSlotInvalidIndex; + Widgets::UIEditorStatusBarState statusBarState = {}; +}; + +struct UIEditorViewportShellSpec { + Widgets::UIEditorViewportSlotChrome chrome = {}; + std::vector toolItems = {}; + std::vector statusSegments = {}; + UIEditorViewportInputBridgeConfig inputBridgeConfig = {}; + UIEditorViewportShellVisualState visualState = {}; +}; + +struct UIEditorViewportShellModel { + UIEditorViewportShellSpec spec = {}; + Widgets::UIEditorViewportSlotFrame frame = {}; +}; + +struct UIEditorViewportShellRequest { + Widgets::UIEditorViewportSlotLayout slotLayout = {}; + ::XCEngine::UI::UISize requestedViewportSize = {}; +}; + +struct UIEditorViewportShellState { + UIEditorViewportInputBridgeState inputBridgeState = {}; +}; + +struct UIEditorViewportShellFrame { + Widgets::UIEditorViewportSlotLayout slotLayout = {}; + Widgets::UIEditorViewportSlotState slotState = {}; + UIEditorViewportInputBridgeFrame inputFrame = {}; + ::XCEngine::UI::UISize requestedViewportSize = {}; +}; + +UIEditorViewportShellRequest ResolveUIEditorViewportShellRequest( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorViewportShellSpec& spec, + const Widgets::UIEditorViewportSlotMetrics& metrics = {}); + +UIEditorViewportShellFrame UpdateUIEditorViewportShell( + UIEditorViewportShellState& state, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorViewportShellModel& model, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Widgets::UIEditorViewportSlotMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Core/UIEditorViewportShell.cpp b/new_editor/src/Core/UIEditorViewportShell.cpp new file mode 100644 index 00000000..5faffcb4 --- /dev/null +++ b/new_editor/src/Core/UIEditorViewportShell.cpp @@ -0,0 +1,72 @@ +#include + +namespace XCEngine::UI::Editor { + +namespace { + +using Widgets::BuildUIEditorViewportSlotLayout; +using Widgets::UIEditorViewportSlotFrame; +using Widgets::UIEditorViewportSlotLayout; +using Widgets::UIEditorViewportSlotState; + +UIEditorViewportSlotLayout BuildViewportShellLayout( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorViewportShellSpec& spec, + const UIEditorViewportSlotFrame& frame, + const Widgets::UIEditorViewportSlotMetrics& metrics) { + return BuildUIEditorViewportSlotLayout( + bounds, + spec.chrome, + frame, + spec.toolItems, + spec.statusSegments, + metrics); +} + +UIEditorViewportSlotState BuildViewportShellSlotState( + const UIEditorViewportShellVisualState& visualState, + const UIEditorViewportInputBridgeFrame& inputFrame) { + UIEditorViewportSlotState slotState = {}; + slotState.focused = inputFrame.focused; + slotState.surfaceHovered = inputFrame.hovered; + slotState.surfaceActive = inputFrame.focused || inputFrame.captured; + slotState.inputCaptured = inputFrame.captured; + slotState.hoveredToolIndex = visualState.hoveredToolIndex; + slotState.activeToolIndex = visualState.activeToolIndex; + slotState.statusBarState = visualState.statusBarState; + slotState.statusBarState.focused = + slotState.statusBarState.focused || inputFrame.focused; + return slotState; +} + +} // namespace + +UIEditorViewportShellRequest ResolveUIEditorViewportShellRequest( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorViewportShellSpec& spec, + const Widgets::UIEditorViewportSlotMetrics& metrics) { + UIEditorViewportShellRequest request = {}; + request.slotLayout = BuildViewportShellLayout(bounds, spec, {}, metrics); + request.requestedViewportSize = request.slotLayout.requestedSurfaceSize; + return request; +} + +UIEditorViewportShellFrame UpdateUIEditorViewportShell( + UIEditorViewportShellState& state, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorViewportShellModel& model, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Widgets::UIEditorViewportSlotMetrics& metrics) { + UIEditorViewportShellFrame frame = {}; + frame.slotLayout = BuildViewportShellLayout(bounds, model.spec, model.frame, metrics); + frame.requestedViewportSize = frame.slotLayout.requestedSurfaceSize; + frame.inputFrame = UpdateUIEditorViewportInputBridge( + state.inputBridgeState, + frame.slotLayout.inputRect, + inputEvents, + model.spec.inputBridgeConfig); + frame.slotState = BuildViewportShellSlotState(model.spec.visualState, frame.inputFrame); + return frame; +} + +} // namespace XCEngine::UI::Editor diff --git a/tests/UI/Editor/integration/CMakeLists.txt b/tests/UI/Editor/integration/CMakeLists.txt index 76ce5161..5d12b4d0 100644 --- a/tests/UI/Editor/integration/CMakeLists.txt +++ b/tests/UI/Editor/integration/CMakeLists.txt @@ -29,6 +29,11 @@ if(TARGET editor_ui_viewport_slot_basic_validation) editor_ui_viewport_slot_basic_validation) endif() +if(TARGET editor_ui_viewport_shell_basic_validation) + list(APPEND EDITOR_UI_INTEGRATION_TARGETS + editor_ui_viewport_shell_basic_validation) +endif() + if(TARGET editor_ui_viewport_input_bridge_basic_validation) list(APPEND EDITOR_UI_INTEGRATION_TARGETS editor_ui_viewport_input_bridge_basic_validation) diff --git a/tests/UI/Editor/integration/README.md b/tests/UI/Editor/integration/README.md index c4bf5c7d..f81e40de 100644 --- a/tests/UI/Editor/integration/README.md +++ b/tests/UI/Editor/integration/README.md @@ -11,6 +11,8 @@ Rules: Layout: +- Primary categories are `shell/` and `state/`. +- `menu`, `workspace`, and `viewport` in scenario names describe contract families, not extra directory levels. - `shared/`: shared host wrapper, scenario registry, shared theme - `shell/workspace_shell_compose/`: split/tab/panel shell compose only - `shell/menu_bar_basic/`: menu bar open/close/hover/dispatch only @@ -18,7 +20,8 @@ Layout: - `shell/panel_frame_basic/`: panel frame layout/state/hit-test only - `shell/status_bar_basic/`: status bar slot/segment/hit-test only - `shell/tab_strip_basic/`: tab strip layout/state/hit-test/close/navigation only -- `shell/viewport_slot_basic/`: viewport shell chrome/surface/status only +- `shell/viewport_slot_basic/`: viewport slot chrome/surface/status only +- `shell/viewport_shell_basic/`: viewport shell request/state compose only - `state/panel_session_flow/`: panel session state flow only - `state/layout_persistence/`: layout save/load/reset only - `state/shortcut_dispatch/`: shortcut match/suppression/dispatch only @@ -61,6 +64,11 @@ Scenarios: Executable: `XCUIEditorViewportSlotBasicValidation.exe` Scope: viewport top bar / surface / status bar layout, hover/focus/active/capture, texture vs fallback only +- `editor.shell.viewport_shell_basic` + Build target: `editor_ui_viewport_shell_basic_validation` + Executable: `XCUIEditorViewportShellBasicValidation.exe` + Scope: `ResolveUIEditorViewportShellRequest(...)` + `UpdateUIEditorViewportShell(...)` basic contract, TopBar / BottomBar request-size sync, input rect + hover/focus/capture state sync only + - `editor.state.panel_session_flow` Build target: `editor_ui_panel_session_flow_validation` Executable: `XCUIEditorPanelSessionFlowValidation.exe` @@ -115,6 +123,9 @@ Selected controls: - `shell/viewport_slot_basic/` Hover toolbar / surface / status bar, click surface to focus, hold and release left mouse to inspect capture, toggle `TopBar / 状态条 / Texture / 方形比例`, press `F12`. +- `shell/viewport_shell_basic/` + Hover / click / drag the viewport shell surface, toggle `TopBar / BottomBar / Texture`, inspect left-side `Request Size / Input Rect / Hover Hit / Hover / Focus / Capture / Result`, press `Reset`, press `截图` or `F12`. + - `state/panel_session_flow/` Click `Hide Active / Show Doc A / Close Doc B / Open Doc B / Activate Details / Reset`, press `F12`. diff --git a/tests/UI/Editor/integration/shell/CMakeLists.txt b/tests/UI/Editor/integration/shell/CMakeLists.txt index 44169b5c..eb1682fe 100644 --- a/tests/UI/Editor/integration/shell/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/CMakeLists.txt @@ -13,3 +13,6 @@ endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/viewport_slot_basic/CMakeLists.txt") add_subdirectory(viewport_slot_basic) endif() +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/viewport_shell_basic/CMakeLists.txt") + add_subdirectory(viewport_shell_basic) +endif() diff --git a/tests/UI/Editor/integration/shell/viewport_shell_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/viewport_shell_basic/CMakeLists.txt new file mode 100644 index 00000000..dc544d20 --- /dev/null +++ b/tests/UI/Editor/integration/shell/viewport_shell_basic/CMakeLists.txt @@ -0,0 +1,30 @@ +add_executable(editor_ui_viewport_shell_basic_validation WIN32 + main.cpp +) + +target_include_directories(editor_ui_viewport_shell_basic_validation PRIVATE + ${CMAKE_SOURCE_DIR}/new_editor/include + ${CMAKE_SOURCE_DIR}/new_editor/app + ${CMAKE_SOURCE_DIR}/engine/include +) + +target_compile_definitions(editor_ui_viewport_shell_basic_validation PRIVATE + UNICODE + _UNICODE + XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}" +) + +if(MSVC) + target_compile_options(editor_ui_viewport_shell_basic_validation PRIVATE /utf-8 /FS) + set_property(TARGET editor_ui_viewport_shell_basic_validation PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +target_link_libraries(editor_ui_viewport_shell_basic_validation PRIVATE + XCUIEditorLib + XCUIEditorHost +) + +set_target_properties(editor_ui_viewport_shell_basic_validation PROPERTIES + OUTPUT_NAME "XCUIEditorViewportShellBasicValidation" +) diff --git a/tests/UI/Editor/integration/shell/viewport_shell_basic/captures/.gitkeep b/tests/UI/Editor/integration/shell/viewport_shell_basic/captures/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/UI/Editor/integration/shell/viewport_shell_basic/captures/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/UI/Editor/integration/shell/viewport_shell_basic/main.cpp b/tests/UI/Editor/integration/shell/viewport_shell_basic/main.cpp new file mode 100644 index 00000000..e12b239f --- /dev/null +++ b/tests/UI/Editor/integration/shell/viewport_shell_basic/main.cpp @@ -0,0 +1,827 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include "Host/AutoScreenshot.h" +#include "Host/InputModifierTracker.h" +#include "Host/NativeRenderer.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT +#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "." +#endif + +namespace { + +using XCEngine::UI::UIColor; +using XCEngine::UI::UIDrawData; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::UISize; +using XCEngine::UI::Editor::ResolveUIEditorViewportShellRequest; +using XCEngine::UI::Editor::UIEditorViewportShellFrame; +using XCEngine::UI::Editor::UIEditorViewportShellModel; +using XCEngine::UI::Editor::UIEditorViewportShellRequest; +using XCEngine::UI::Editor::UIEditorViewportShellSpec; +using XCEngine::UI::Editor::UIEditorViewportShellState; +using XCEngine::UI::Editor::UpdateUIEditorViewportShell; +using XCEngine::UI::Editor::Host::AutoScreenshotController; +using XCEngine::UI::Editor::Host::InputModifierTracker; +using XCEngine::UI::Editor::Host::NativeRenderer; +using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotBackground; +using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotForeground; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorViewportSlot; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotFrame; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTarget; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolItem; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot; + +constexpr const wchar_t* kWindowClassName = L"XCUIEditorViewportShellBasicValidation"; +constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ViewportShell Basic"; + +constexpr UIColor kWindowBg(0.12f, 0.12f, 0.12f, 1.0f); +constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f); +constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f); +constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f); +constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f); +constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f); +constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f); +constexpr UIColor kButtonOnBg(0.39f, 0.39f, 0.39f, 1.0f); +constexpr UIColor kButtonBorder(0.47f, 0.47f, 0.47f, 1.0f); +constexpr UIColor kPreviewBg(0.15f, 0.15f, 0.15f, 1.0f); + +enum class ActionId : unsigned char { + ToggleTopBar = 0, + ToggleBottomBar, + ToggleTexture, + Reset, + Capture +}; + +struct ButtonState { + ActionId action = ActionId::ToggleTopBar; + std::string label = {}; + UIRect rect = {}; + bool selected = false; +}; + +std::filesystem::path ResolveRepoRootPath() { + std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT; + if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { + root = root.substr(1u, root.size() - 2u); + } + + return std::filesystem::path(root).lexically_normal(); +} + +bool ContainsPoint(const UIRect& rect, float x, float y) { + return x >= rect.x && + x <= rect.x + rect.width && + y >= rect.y && + y <= rect.y + rect.height; +} + +std::string BoolText(bool value) { + return value ? "On" : "Off"; +} + +std::string FormatFloat(float value) { + std::ostringstream stream = {}; + stream.setf(std::ios::fixed, std::ios::floatfield); + stream.precision(1); + stream << value; + return stream.str(); +} + +std::string FormatSize(const UISize& size) { + return FormatFloat(size.width) + " x " + FormatFloat(size.height); +} + +std::string FormatRect(const UIRect& rect) { + return "x=" + FormatFloat(rect.x) + + " y=" + FormatFloat(rect.y) + + " w=" + FormatFloat(rect.width) + + " h=" + FormatFloat(rect.height); +} + +std::string DescribeHitTarget(const UIEditorViewportSlotHitTarget& hit) { + switch (hit.kind) { + case UIEditorViewportSlotHitTargetKind::TopBar: + return "TopBar"; + case UIEditorViewportSlotHitTargetKind::Title: + return "Title"; + case UIEditorViewportSlotHitTargetKind::ToolItem: + return "ToolItem[" + std::to_string(hit.index) + "]"; + case UIEditorViewportSlotHitTargetKind::Surface: + return "Surface"; + case UIEditorViewportSlotHitTargetKind::BottomBar: + return "BottomBar"; + case UIEditorViewportSlotHitTargetKind::StatusSegment: + return "StatusSegment[" + std::to_string(hit.index) + "]"; + case UIEditorViewportSlotHitTargetKind::StatusSeparator: + return "StatusSeparator[" + std::to_string(hit.index) + "]"; + case UIEditorViewportSlotHitTargetKind::None: + default: + return "None"; + } +} + +void DrawCard( + UIDrawList& drawList, + const UIRect& rect, + std::string_view title, + std::string_view subtitle = {}) { + drawList.AddFilledRect(rect, kCardBg, 10.0f); + drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 14.0f), + std::string(title), + kTextPrimary, + 17.0f); + if (!subtitle.empty()) { + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 38.0f), + std::string(subtitle), + kTextMuted, + 12.0f); + } +} + +void DrawButton(UIDrawList& drawList, const ButtonState& button) { + drawList.AddFilledRect( + button.rect, + button.selected ? kButtonOnBg : kButtonBg, + 8.0f); + drawList.AddRectOutline(button.rect, kButtonBorder, 1.0f, 8.0f); + drawList.AddText( + UIPoint(button.rect.x + 12.0f, button.rect.y + 10.0f), + button.label, + kTextPrimary, + 12.0f); +} + +class ScenarioApp { +public: + int Run(HINSTANCE hInstance, int nCmdShow) { + if (!Initialize(hInstance, nCmdShow)) { + Shutdown(); + return 1; + } + + MSG message = {}; + while (message.message != WM_QUIT) { + if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { + TranslateMessage(&message); + DispatchMessageW(&message); + continue; + } + + RenderFrame(); + Sleep(8); + } + + Shutdown(); + return static_cast(message.wParam); + } + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + const auto* createStruct = reinterpret_cast(lParam); + auto* app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + return TRUE; + } + + auto* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (message) { + case WM_SIZE: + if (app != nullptr && wParam != SIZE_MINIMIZED) { + app->m_renderer.Resize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + case WM_MOUSEMOVE: + if (app != nullptr) { + app->QueuePointerEvent(UIInputEventType::PointerMove, UIPointerButton::None, wParam, lParam); + TRACKMOUSEEVENT event = {}; + event.cbSize = sizeof(event); + event.dwFlags = TME_LEAVE; + event.hwndTrack = hwnd; + TrackMouseEvent(&event); + return 0; + } + break; + case WM_MOUSELEAVE: + if (app != nullptr) { + app->QueuePointerLeaveEvent(); + return 0; + } + break; + case WM_LBUTTONDOWN: + if (app != nullptr) { + SetFocus(hwnd); + app->HandlePointerDown(UIPointerButton::Left, wParam, lParam); + return 0; + } + break; + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandlePointerUp(UIPointerButton::Left, wParam, lParam); + return 0; + } + break; + case WM_MOUSEWHEEL: + if (app != nullptr) { + app->QueuePointerWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam), wParam, lParam); + return 0; + } + break; + case WM_SETFOCUS: + if (app != nullptr) { + app->m_inputModifierTracker.SyncFromSystemState(); + app->QueueFocusEvent(UIInputEventType::FocusGained); + return 0; + } + break; + case WM_KILLFOCUS: + if (app != nullptr) { + if (GetCapture() == hwnd) { + ReleaseCapture(); + } + app->m_inputModifierTracker.Reset(); + app->QueueFocusEvent(UIInputEventType::FocusLost); + return 0; + } + break; + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr && wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + InvalidateRect(hwnd, nullptr, FALSE); + UpdateWindow(hwnd); + return 0; + } + break; + case WM_PAINT: + if (app != nullptr) { + PAINTSTRUCT paintStruct = {}; + BeginPaint(hwnd, &paintStruct); + app->RenderFrame(); + EndPaint(hwnd, &paintStruct); + return 0; + } + break; + case WM_ERASEBKGND: + return 1; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + default: + break; + } + + return DefWindowProcW(hwnd, message, wParam, lParam); + } + + bool Initialize(HINSTANCE hInstance, int nCmdShow) { + m_captureRoot = + ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/viewport_shell_basic/captures"; + m_autoScreenshot.Initialize(m_captureRoot); + + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(windowClass); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = &ScenarioApp::WndProc; + windowClass.hInstance = hInstance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.lpszClassName = kWindowClassName; + + m_windowClassAtom = RegisterClassExW(&windowClass); + if (m_windowClassAtom == 0) { + return false; + } + + m_hwnd = CreateWindowExW( + 0, + kWindowClassName, + kWindowTitle, + WS_OVERLAPPEDWINDOW | WS_VISIBLE, + CW_USEDEFAULT, + CW_USEDEFAULT, + 1520, + 940, + nullptr, + nullptr, + hInstance, + this); + if (m_hwnd == nullptr) { + return false; + } + + if (!m_renderer.Initialize(m_hwnd)) { + return false; + } + + ShowWindow(m_hwnd, nCmdShow); + ResetScenario(); + return true; + } + + void Shutdown() { + m_autoScreenshot.Shutdown(); + m_renderer.Shutdown(); + + if (m_hwnd != nullptr && IsWindow(m_hwnd)) { + DestroyWindow(m_hwnd); + } + m_hwnd = nullptr; + + if (m_windowClassAtom != 0) { + UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr)); + m_windowClassAtom = 0; + } + } + + void ResetScenario() { + m_showTopBar = true; + m_showBottomBar = true; + m_textureEnabled = true; + m_inputModifierTracker.Reset(); + m_shellState = {}; + m_shellRequest = {}; + m_shellFrame = {}; + m_shellSpec = {}; + m_shellModel = {}; + m_hoverHit = {}; + m_pendingEvents.clear(); + m_lastResult = "Ready"; + m_hasSnapshot = false; + } + + void QueuePointerEvent(UIInputEventType type, UIPointerButton button, WPARAM wParam, LPARAM lParam) { + UIInputEvent event = {}; + event.type = type; + event.pointerButton = button; + event.position = UIPoint( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast(wParam)); + m_pendingEvents.push_back(event); + m_mousePosition = event.position; + } + + void QueuePointerLeaveEvent() { + UIInputEvent event = {}; + event.type = UIInputEventType::PointerLeave; + if (m_hwnd != nullptr) { + POINT clientPoint = {}; + GetCursorPos(&clientPoint); + ScreenToClient(m_hwnd, &clientPoint); + event.position = UIPoint(static_cast(clientPoint.x), static_cast(clientPoint.y)); + m_mousePosition = event.position; + } + m_pendingEvents.push_back(event); + } + + void QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam) { + if (m_hwnd == nullptr) { + return; + } + + POINT screenPoint = { + GET_X_LPARAM(lParam), + GET_Y_LPARAM(lParam) + }; + ScreenToClient(m_hwnd, &screenPoint); + + UIInputEvent event = {}; + event.type = UIInputEventType::PointerWheel; + event.position = UIPoint(static_cast(screenPoint.x), static_cast(screenPoint.y)); + event.wheelDelta = static_cast(wheelDelta); + event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast(wParam)); + m_pendingEvents.push_back(event); + m_mousePosition = event.position; + } + + void QueueFocusEvent(UIInputEventType type) { + UIInputEvent event = {}; + event.type = type; + event.modifiers = m_inputModifierTracker.GetCurrentModifiers(); + m_pendingEvents.push_back(event); + } + + void HandlePointerDown(UIPointerButton button, WPARAM wParam, LPARAM lParam) { + QueuePointerEvent(UIInputEventType::PointerButtonDown, button, wParam, lParam); + const UIPoint point( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + + if (ContainsPoint(m_slotRect, point.x, point.y) && + ContainsPoint(m_shellFrame.slotLayout.inputRect, point.x, point.y)) { + SetCapture(m_hwnd); + } + } + + void HandlePointerUp(UIPointerButton button, WPARAM wParam, LPARAM lParam) { + QueuePointerEvent(UIInputEventType::PointerButtonUp, button, wParam, lParam); + + const UIPoint point( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + + if (GetCapture() == m_hwnd) { + ReleaseCapture(); + } + + for (const ButtonState& buttonState : m_buttons) { + if (!ContainsPoint(buttonState.rect, point.x, point.y)) { + continue; + } + + ExecuteAction(buttonState.action); + break; + } + } + + std::vector BuildToolItems() const { + return { + { "mode", "Perspective", UIEditorViewportSlotToolSlot::Leading, true, true, 98.0f }, + { "focus", "ViewportShell", UIEditorViewportSlotToolSlot::Trailing, true, false, 108.0f } + }; + } + + std::vector BuildStatusSegments() const { + return { + { + "chrome-top", + std::string("TopBar ") + BoolText(m_showTopBar), + UIEditorStatusBarSlot::Leading, + {}, + true, + true, + 96.0f + }, + { + "chrome-bottom", + std::string("BottomBar ") + BoolText(m_showBottomBar), + UIEditorStatusBarSlot::Leading, + {}, + true, + false, + 120.0f + }, + { + "frame", + m_textureEnabled ? "Texture On" : "Fallback", + UIEditorStatusBarSlot::Trailing, + {}, + true, + true, + 108.0f + } + }; + } + + UIEditorViewportShellSpec BuildShellSpec() const { + UIEditorViewportShellSpec spec = {}; + spec.chrome.title = "Scene View"; + spec.chrome.subtitle = "ViewportShell 基础层"; + spec.chrome.showTopBar = m_showTopBar; + spec.chrome.showBottomBar = m_showBottomBar; + spec.chrome.topBarHeight = 40.0f; + spec.chrome.bottomBarHeight = 28.0f; + spec.toolItems = BuildToolItems(); + spec.statusSegments = BuildStatusSegments(); + return spec; + } + + UIEditorViewportSlotFrame BuildViewportFrame(const UISize& requestedSize) const { + UIEditorViewportSlotFrame frame = {}; + frame.requestedSize = requestedSize; + if (m_textureEnabled) { + frame.hasTexture = true; + frame.texture = { 1u, 1280u, 720u }; + frame.presentedSize = UISize(1280.0f, 720.0f); + frame.statusText = "Fake viewport frame"; + } else { + frame.hasTexture = false; + frame.statusText = "这里只验证 ViewportShell contract,不接 Scene/Game 业务。"; + } + return frame; + } + + void UpdateLayoutForCurrentWindow() { + if (m_hwnd == nullptr) { + return; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + + const float leftColumnWidth = 460.0f; + const float outerPadding = 20.0f; + m_introRect = UIRect(outerPadding, outerPadding, leftColumnWidth, 246.0f); + m_controlsRect = UIRect(outerPadding, 286.0f, leftColumnWidth, 246.0f); + m_stateRect = UIRect(outerPadding, 552.0f, leftColumnWidth, height - 572.0f); + m_previewRect = UIRect( + leftColumnWidth + outerPadding * 2.0f, + outerPadding, + width - leftColumnWidth - outerPadding * 3.0f, + height - outerPadding * 2.0f); + m_slotRect = UIRect( + m_previewRect.x + 18.0f, + m_previewRect.y + 18.0f, + m_previewRect.width - 36.0f, + m_previewRect.height - 36.0f); + + const float buttonHeight = 34.0f; + const float gap = 10.0f; + const float left = m_controlsRect.x + 16.0f; + const float top = m_controlsRect.y + 54.0f; + const float widthAvailable = m_controlsRect.width - 32.0f; + m_buttons = { + { + ActionId::ToggleTopBar, + std::string("TopBar: ") + (m_showTopBar ? "开" : "关"), + UIRect(left, top, widthAvailable, buttonHeight), + m_showTopBar + }, + { + ActionId::ToggleBottomBar, + std::string("BottomBar: ") + (m_showBottomBar ? "开" : "关"), + UIRect(left, top + (buttonHeight + gap), widthAvailable, buttonHeight), + m_showBottomBar + }, + { + ActionId::ToggleTexture, + std::string("Texture: ") + (m_textureEnabled ? "开" : "关"), + UIRect(left, top + (buttonHeight + gap) * 2.0f, widthAvailable, buttonHeight), + m_textureEnabled + }, + { + ActionId::Reset, + "Reset", + UIRect(left, top + (buttonHeight + gap) * 3.0f, widthAvailable, buttonHeight), + false + }, + { + ActionId::Capture, + "截图", + UIRect(left, top + (buttonHeight + gap) * 4.0f, widthAvailable, buttonHeight), + false + } + }; + } + + void ExecuteAction(ActionId action) { + switch (action) { + case ActionId::ToggleTopBar: + m_showTopBar = !m_showTopBar; + m_lastResult = m_showTopBar ? "TopBar 已打开" : "TopBar 已关闭"; + break; + case ActionId::ToggleBottomBar: + m_showBottomBar = !m_showBottomBar; + m_lastResult = m_showBottomBar ? "BottomBar 已打开" : "BottomBar 已关闭"; + break; + case ActionId::ToggleTexture: + m_textureEnabled = !m_textureEnabled; + m_lastResult = m_textureEnabled ? "切到 Texture 分支" : "切到 Fallback 分支"; + break; + case ActionId::Reset: + ResetScenario(); + m_lastResult = "状态已重置"; + break; + case ActionId::Capture: + m_autoScreenshot.RequestCapture("manual_button"); + InvalidateRect(m_hwnd, nullptr, FALSE); + UpdateWindow(m_hwnd); + m_lastResult = "截图已排队"; + break; + } + } + + void UpdateLastResult() { + const auto& inputFrame = m_shellFrame.inputFrame; + if (inputFrame.captureStarted) { + m_lastResult = "CaptureStarted"; + } else if (inputFrame.captureEnded) { + m_lastResult = "CaptureEnded"; + } else if (inputFrame.focusLost) { + m_lastResult = "FocusLost"; + } else if (inputFrame.focusGained) { + m_lastResult = "FocusGained"; + } else if (inputFrame.pointerPressedInside) { + m_lastResult = "PointerDownInside"; + } else if (inputFrame.pointerReleasedInside) { + m_lastResult = "PointerUpInside"; + } else if (inputFrame.wheelDelta != 0.0f) { + m_lastResult = "Wheel " + FormatFloat(inputFrame.wheelDelta); + } else if (m_hasSnapshot && m_previousHovered != inputFrame.hovered) { + m_lastResult = std::string("Hover ") + BoolText(inputFrame.hovered); + } + + m_previousHovered = inputFrame.hovered; + m_previousFocused = inputFrame.focused; + m_previousCaptured = inputFrame.captured; + m_hasSnapshot = true; + } + + void UpdateScenarioFrame() { + m_shellSpec = BuildShellSpec(); + m_shellRequest = ResolveUIEditorViewportShellRequest(m_slotRect, m_shellSpec); + m_shellModel = {}; + m_shellModel.spec = m_shellSpec; + m_shellModel.frame = BuildViewportFrame(m_shellRequest.requestedViewportSize); + + std::vector frameEvents = std::move(m_pendingEvents); + m_pendingEvents.clear(); + m_shellFrame = UpdateUIEditorViewportShell( + m_shellState, + m_slotRect, + m_shellModel, + frameEvents); + m_hoverHit = HitTestUIEditorViewportSlot(m_shellFrame.slotLayout, m_mousePosition); + UpdateLastResult(); + } + + void RenderFrame() { + if (m_hwnd == nullptr) { + return; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + + UpdateLayoutForCurrentWindow(); + UpdateScenarioFrame(); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("ViewportShellBasic"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg); + + DrawCard( + drawList, + m_introRect, + "测试功能:ViewportShell 基础 contract", + "只验证 Resolve + Update,不接 Scene/Game 业务面板。"); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 66.0f), + "重点检查:切换 TopBar / BottomBar 后,Request Size 与 Input Rect 要同步变化。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 88.0f), + "重点检查:Hover / Focus / Capture 要和右侧 surface 边框状态一致。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 110.0f), + "重点检查:Texture 与 Fallback 只影响 frame 分支,不应破坏 shell 输入边界。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 132.0f), + "操作:hover Surface,click 获取 Focus,按住拖出再松开,检查 Capture。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 154.0f), + "操作:点击左侧 TopBar / BottomBar / Texture,观察左侧状态与右侧布局。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 176.0f), + "操作:需要保留当前画面时点“截图”,或直接按 F12。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 204.0f), + "结果判定:左侧 Request / Input / HoverHit / Hover / Focus / Capture 必须一致。", + kTextWeak, + 11.0f); + + DrawCard(drawList, m_controlsRect, "操作", "只保留当前测试真正需要检查的 5 个控件。"); + for (const ButtonState& button : m_buttons) { + DrawButton(drawList, button); + } + + DrawCard(drawList, m_stateRect, "状态", "重点看 request、input rect、hover 命中与输入桥同步。"); + float stateY = m_stateRect.y + 66.0f; + auto addStateLine = [&](std::string text, const UIColor& color, float fontSize = 12.0f) { + drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY), std::move(text), color, fontSize); + stateY += 22.0f; + }; + + addStateLine("TopBar: " + BoolText(m_showTopBar), kTextPrimary); + addStateLine("BottomBar: " + BoolText(m_showBottomBar), kTextPrimary); + addStateLine("Texture: " + BoolText(m_textureEnabled), kTextPrimary); + addStateLine("Request Size: " + FormatSize(m_shellRequest.requestedViewportSize), kTextPrimary); + addStateLine("Input Rect: " + FormatRect(m_shellFrame.slotLayout.inputRect), kTextMuted, 11.0f); + addStateLine("Hover Hit: " + DescribeHitTarget(m_hoverHit), kTextPrimary); + addStateLine("Hover: " + BoolText(m_shellFrame.inputFrame.hovered), kTextPrimary); + addStateLine("Focus: " + BoolText(m_shellFrame.inputFrame.focused), kTextPrimary); + addStateLine("Capture: " + BoolText(m_shellFrame.inputFrame.captured), kTextPrimary); + addStateLine("Result: " + m_lastResult, kTextMuted); + + const std::string captureSummary = + m_autoScreenshot.HasPendingCapture() + ? "截图排队中..." + : (m_autoScreenshot.GetLastCaptureSummary().empty() + ? std::string("截图:F12 或按钮 -> viewport_shell_basic/captures/") + : m_autoScreenshot.GetLastCaptureSummary()); + addStateLine(captureSummary, kTextWeak, 11.0f); + + DrawCard(drawList, m_previewRect, "Preview", "这里只有一个 ViewportShell,用来检验 Editor 基础层 compose。"); + drawList.AddFilledRect( + UIRect( + m_previewRect.x + 12.0f, + m_previewRect.y + 44.0f, + m_previewRect.width - 24.0f, + m_previewRect.height - 56.0f), + kPreviewBg, + 10.0f); + + AppendUIEditorViewportSlotBackground( + drawList, + m_shellFrame.slotLayout, + m_shellModel.spec.toolItems, + m_shellModel.spec.statusSegments, + m_shellFrame.slotState); + AppendUIEditorViewportSlotForeground( + drawList, + m_shellFrame.slotLayout, + m_shellModel.spec.chrome, + m_shellModel.frame, + m_shellModel.spec.toolItems, + m_shellModel.spec.statusSegments, + m_shellFrame.slotState); + + const bool framePresented = m_renderer.Render(drawData); + m_autoScreenshot.CaptureIfRequested( + m_renderer, + drawData, + static_cast(width), + static_cast(height), + framePresented); + } + + HWND m_hwnd = nullptr; + ATOM m_windowClassAtom = 0; + NativeRenderer m_renderer = {}; + AutoScreenshotController m_autoScreenshot = {}; + InputModifierTracker m_inputModifierTracker = {}; + std::filesystem::path m_captureRoot = {}; + std::vector m_pendingEvents = {}; + std::vector m_buttons = {}; + UIEditorViewportShellState m_shellState = {}; + UIEditorViewportShellRequest m_shellRequest = {}; + UIEditorViewportShellFrame m_shellFrame = {}; + UIEditorViewportShellSpec m_shellSpec = {}; + UIEditorViewportShellModel m_shellModel = {}; + UIEditorViewportSlotHitTarget m_hoverHit = {}; + UIRect m_introRect = {}; + UIRect m_controlsRect = {}; + UIRect m_stateRect = {}; + UIRect m_previewRect = {}; + UIRect m_slotRect = {}; + UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f); + bool m_showTopBar = true; + bool m_showBottomBar = true; + bool m_textureEnabled = true; + bool m_previousHovered = false; + bool m_previousFocused = false; + bool m_previousCaptured = false; + bool m_hasSnapshot = false; + std::string m_lastResult = {}; +}; + +} // namespace + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { + return ScenarioApp().Run(hInstance, nCmdShow); +} diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index 0e10eac0..4499a321 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -16,6 +16,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES test_ui_editor_status_bar.cpp test_ui_editor_tab_strip.cpp test_ui_editor_viewport_input_bridge.cpp + test_ui_editor_viewport_shell.cpp test_ui_editor_viewport_slot.cpp test_ui_editor_shortcut_manager.cpp test_ui_editor_workspace_controller.cpp diff --git a/tests/UI/Editor/unit/test_ui_editor_viewport_shell.cpp b/tests/UI/Editor/unit/test_ui_editor_viewport_shell.cpp new file mode 100644 index 00000000..9ed03c72 --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_viewport_shell.cpp @@ -0,0 +1,132 @@ +#include + +#include + +namespace { + +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::ResolveUIEditorViewportShellRequest; +using XCEngine::UI::Editor::UIEditorViewportShellModel; +using XCEngine::UI::Editor::UIEditorViewportShellSpec; +using XCEngine::UI::Editor::UIEditorViewportShellState; +using XCEngine::UI::Editor::UpdateUIEditorViewportShell; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarInvalidIndex; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarState; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotInvalidIndex; + +UIInputEvent MakePointerEvent( + UIInputEventType type, + float x, + float y, + UIPointerButton button = UIPointerButton::None) { + UIInputEvent event = {}; + event.type = type; + event.position = UIPoint(x, y); + event.pointerButton = button; + return event; +} + +TEST(UIEditorViewportShellTest, ResolveRequestUsesViewportInputRectSize) { + UIEditorViewportShellSpec spec = {}; + const auto request = ResolveUIEditorViewportShellRequest( + UIRect(10.0f, 20.0f, 800.0f, 600.0f), + spec); + + EXPECT_FLOAT_EQ(request.slotLayout.inputRect.width, 800.0f); + EXPECT_FLOAT_EQ(request.slotLayout.inputRect.height, 532.0f); + EXPECT_FLOAT_EQ(request.requestedViewportSize.width, 800.0f); + EXPECT_FLOAT_EQ(request.requestedViewportSize.height, 532.0f); +} + +TEST(UIEditorViewportShellTest, ResolveRequestTracksChromeBarVisibility) { + UIEditorViewportShellSpec spec = {}; + spec.chrome.showTopBar = false; + spec.chrome.showBottomBar = false; + + const auto request = ResolveUIEditorViewportShellRequest( + UIRect(10.0f, 20.0f, 800.0f, 600.0f), + spec); + + EXPECT_FLOAT_EQ(request.slotLayout.inputRect.height, 600.0f); + EXPECT_FLOAT_EQ(request.requestedViewportSize.height, 600.0f); +} + +TEST(UIEditorViewportShellTest, UpdateShellMapsInputBridgeStateToViewportSlotState) { + UIEditorViewportShellModel model = {}; + UIEditorViewportShellState state = {}; + const auto request = ResolveUIEditorViewportShellRequest( + UIRect(10.0f, 20.0f, 800.0f, 600.0f), + model.spec); + + const float insideX = request.slotLayout.inputRect.x + 80.0f; + const float insideY = request.slotLayout.inputRect.y + 60.0f; + const auto frame = UpdateUIEditorViewportShell( + state, + UIRect(10.0f, 20.0f, 800.0f, 600.0f), + model, + { + MakePointerEvent(UIInputEventType::PointerMove, insideX, insideY), + MakePointerEvent(UIInputEventType::PointerButtonDown, insideX, insideY, UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.inputFrame.focused); + EXPECT_TRUE(frame.inputFrame.captured); + EXPECT_TRUE(frame.slotState.focused); + EXPECT_TRUE(frame.slotState.surfaceHovered); + EXPECT_TRUE(frame.slotState.surfaceActive); + EXPECT_TRUE(frame.slotState.inputCaptured); + EXPECT_TRUE(frame.slotState.statusBarState.focused); + EXPECT_FLOAT_EQ(frame.requestedViewportSize.width, request.requestedViewportSize.width); + EXPECT_FLOAT_EQ(frame.requestedViewportSize.height, request.requestedViewportSize.height); +} + +TEST(UIEditorViewportShellTest, UpdateShellPreservesVisualOverrides) { + UIEditorViewportShellModel model = {}; + model.spec.visualState.hoveredToolIndex = 1u; + model.spec.visualState.activeToolIndex = 2u; + model.spec.visualState.statusBarState = UIEditorStatusBarState{ + 3u, + 4u, + true + }; + + UIEditorViewportShellState state = {}; + const auto frame = UpdateUIEditorViewportShell( + state, + UIRect(10.0f, 20.0f, 800.0f, 600.0f), + model, + {}); + + EXPECT_EQ(frame.slotState.hoveredToolIndex, 1u); + EXPECT_EQ(frame.slotState.activeToolIndex, 2u); + EXPECT_EQ(frame.slotState.statusBarState.hoveredIndex, 3u); + EXPECT_EQ(frame.slotState.statusBarState.activeIndex, 4u); + EXPECT_TRUE(frame.slotState.statusBarState.focused); + EXPECT_FALSE(frame.slotState.focused); + EXPECT_FALSE(frame.slotState.surfaceHovered); + EXPECT_FALSE(frame.slotState.surfaceActive); + EXPECT_FALSE(frame.slotState.inputCaptured); +} + +TEST(UIEditorViewportShellTest, UpdateShellDoesNotIntroduceInvalidIndicesByDefault) { + UIEditorViewportShellModel model = {}; + UIEditorViewportShellState state = {}; + + const auto frame = UpdateUIEditorViewportShell( + state, + UIRect(10.0f, 20.0f, 800.0f, 600.0f), + model, + {}); + + EXPECT_EQ(frame.slotState.hoveredToolIndex, UIEditorViewportSlotInvalidIndex); + EXPECT_EQ(frame.slotState.activeToolIndex, UIEditorViewportSlotInvalidIndex); + EXPECT_EQ(frame.slotState.statusBarState.hoveredIndex, UIEditorStatusBarInvalidIndex); + EXPECT_EQ(frame.slotState.statusBarState.activeIndex, UIEditorStatusBarInvalidIndex); + EXPECT_FALSE(frame.slotState.statusBarState.focused); +} + +} // namespace