diff --git a/new_editor/src/Core/UIEditorShellInteraction.cpp b/new_editor/src/Core/UIEditorShellInteraction.cpp index eed82475..e2dbc3cd 100644 --- a/new_editor/src/Core/UIEditorShellInteraction.cpp +++ b/new_editor/src/Core/UIEditorShellInteraction.cpp @@ -137,7 +137,10 @@ const std::vector* ResolvePopupItems( const UIEditorResolvedMenuItem* item = FindResolvedMenuItemRecursive(menu->items, popupState.itemId); - if (item == nullptr || item->kind != UIEditorMenuItemKind::Submenu) { + if (item == nullptr || + item->kind != UIEditorMenuItemKind::Submenu || + !item->enabled || + item->children.empty()) { return nullptr; } diff --git a/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp index 8c146c7d..fef3285a 100644 --- a/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_shell_interaction.cpp @@ -18,6 +18,7 @@ using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; +using XCEngine::UI::Editor::FindUIEditorWorkspacePanelPresentationState; using XCEngine::UI::Editor::ResolveUIEditorShellInteractionRequest; using XCEngine::UI::Editor::UIEditorCommandDispatchStatus; using XCEngine::UI::Editor::UIEditorCommandDispatcher; @@ -479,6 +480,69 @@ TEST(UIEditorShellInteractionTest, DisabledSubmenuDoesNotOpenChildPopup) { EXPECT_TRUE(state.menuSession.HasOpenMenu()); } +TEST(UIEditorShellInteractionTest, HoverDisabledSubmenuSiblingClosesOnlyOpenChildChain) { + auto controller = BuildController(); + auto model = BuildInteractionModel(); + UIEditorResolvedMenuItem disabledSibling = {}; + disabledSibling.kind = UIEditorMenuItemKind::Submenu; + disabledSibling.itemId = "file-disabled-tools"; + disabledSibling.label = "Disabled Tools"; + disabledSibling.enabled = false; + disabledSibling.children = { + UIEditorResolvedMenuItem{ + UIEditorMenuItemKind::Command, + "file-disabled-child", + "Disabled Child", + "workspace.close", + "Disabled Child", + {}, + true, + false + } + }; + model.resolvedMenuModel.menus[0].items.push_back(disabledSibling); + + UIEditorShellInteractionState state = {}; + auto frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); + const auto* submenuItem = FindPopupItem(frame, "file-workspace-tools"); + const auto* disabledSiblingItem = FindPopupItem(frame, "file-disabled-tools"); + ASSERT_NE(submenuItem, nullptr); + ASSERT_NE(disabledSiblingItem, nullptr); + const UIRect submenuRect = submenuItem->rect; + + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakePointerMove(RectCenter(submenuRect)) }); + ASSERT_EQ(frame.request.popupRequests.size(), 2u); + const std::string childPopupId = frame.request.popupRequests[1].popupId; + disabledSiblingItem = FindPopupItem(frame, "file-disabled-tools"); + ASSERT_NE(disabledSiblingItem, nullptr); + const UIRect disabledSiblingRect = disabledSiblingItem->rect; + + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakePointerMove(RectCenter(disabledSiblingRect)) }); + + EXPECT_TRUE(frame.result.menuMutation.changed); + ASSERT_EQ(frame.result.menuMutation.closedPopupIds.size(), 1u); + EXPECT_EQ(frame.result.menuMutation.closedPopupIds.front(), childPopupId); + EXPECT_EQ(frame.openRootMenuId, "file"); + ASSERT_EQ(frame.request.popupRequests.size(), 1u); + EXPECT_EQ(frame.request.popupRequests.front().menuId, "file"); + EXPECT_TRUE(state.menuSession.HasOpenMenu()); +} + TEST(UIEditorShellInteractionTest, FocusLostDismissesWholeMenuChain) { auto controller = BuildController(); const auto model = BuildInteractionModel(); @@ -515,6 +579,77 @@ TEST(UIEditorShellInteractionTest, FocusLostDismissesWholeMenuChain) { EXPECT_TRUE(frame.request.popupRequests.empty()); } +TEST(UIEditorShellInteractionTest, FocusLostWhileMenuOpenReleasesExistingViewportCapture) { + auto controller = BuildController(); + const auto model = BuildInteractionModel(); + + UIEditorShellInteractionState state = {}; + auto frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + {}); + const auto* fileButton = FindMenuButton(frame, "file"); + ASSERT_NE(fileButton, nullptr); + const UIRect fileButtonRect = fileButton->rect; + ASSERT_FALSE(frame.shellFrame.workspaceFrame.viewportFrames.empty()); + + const UIRect inputRect = + frame.shellFrame.workspaceFrame.viewportFrames.front().viewportShellFrame.slotLayout.inputRect; + const UIPoint center = RectCenter(inputRect); + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { + MakePointerMove(center), + MakeLeftPointerDown(center) + }); + ASSERT_TRUE(frame.result.requestPointerCapture); + const auto* sceneStateBeforeMenu = FindUIEditorWorkspacePanelPresentationState( + state.workspaceInteractionState.composeState, + "scene"); + ASSERT_NE(sceneStateBeforeMenu, nullptr); + ASSERT_TRUE(sceneStateBeforeMenu->viewportShellState.inputBridgeState.captured); + + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakeLeftPointerDown(RectCenter(fileButtonRect)) }); + ASSERT_TRUE(state.menuSession.HasOpenMenu()); + const auto* sceneStateWithMenu = FindUIEditorWorkspacePanelPresentationState( + state.workspaceInteractionState.composeState, + "scene"); + ASSERT_NE(sceneStateWithMenu, nullptr); + ASSERT_TRUE(sceneStateWithMenu->viewportShellState.inputBridgeState.captured); + + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakeFocusLost() }); + + EXPECT_TRUE(frame.result.consumed); + EXPECT_TRUE(frame.result.menuMutation.changed); + EXPECT_EQ(frame.result.menuMutation.dismissReason, UIPopupDismissReason::FocusLoss); + EXPECT_TRUE(frame.result.releasePointerCapture); + EXPECT_TRUE(frame.result.workspaceResult.releasePointerCapture); + EXPECT_EQ(frame.result.viewportPanelId, "scene"); + EXPECT_TRUE(frame.result.viewportInputFrame.focusLost); + EXPECT_TRUE(frame.result.viewportInputFrame.captureEnded); + EXPECT_FALSE(state.menuSession.HasOpenMenu()); + const auto* sceneStateAfterFocusLoss = FindUIEditorWorkspacePanelPresentationState( + state.workspaceInteractionState.composeState, + "scene"); + ASSERT_NE(sceneStateAfterFocusLoss, nullptr); + EXPECT_FALSE(sceneStateAfterFocusLoss->viewportShellState.inputBridgeState.captured); +} + TEST(UIEditorShellInteractionTest, OpenMenuConsumesWorkspacePointerDownForThatFrame) { auto controller = BuildController(); const auto model = BuildInteractionModel(); @@ -648,3 +783,77 @@ TEST(UIEditorShellInteractionTest, InvalidResolvedMenuStateClosesInvisibleModalC EXPECT_FALSE(state.menuSession.HasOpenMenu()); EXPECT_TRUE(frame.request.popupRequests.empty()); } + +TEST(UIEditorShellInteractionTest, OpenedSubmenuClosesWhenSourceItemBecomesDisabled) { + auto controller = BuildController(); + auto model = BuildInteractionModel(); + + UIEditorShellInteractionState state = {}; + auto frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); + const auto* submenuItem = FindPopupItem(frame, "file-workspace-tools"); + ASSERT_NE(submenuItem, nullptr); + + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakePointerMove(RectCenter(submenuItem->rect)) }); + ASSERT_EQ(frame.request.popupRequests.size(), 2u); + + model.resolvedMenuModel.menus[0].items[0].enabled = false; + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + {}); + + EXPECT_TRUE(frame.result.consumed); + EXPECT_TRUE(frame.result.menuMutation.changed); + EXPECT_EQ(frame.result.menuMutation.dismissReason, UIPopupDismissReason::Programmatic); + EXPECT_FALSE(state.menuSession.HasOpenMenu()); + EXPECT_TRUE(frame.request.popupRequests.empty()); +} + +TEST(UIEditorShellInteractionTest, OpenedSubmenuClosesWhenSourceItemLosesChildren) { + auto controller = BuildController(); + auto model = BuildInteractionModel(); + + UIEditorShellInteractionState state = {}; + auto frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakeLeftPointerDown(UIPoint(40.0f, 30.0f)) }); + const auto* submenuItem = FindPopupItem(frame, "file-workspace-tools"); + ASSERT_NE(submenuItem, nullptr); + + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { MakePointerMove(RectCenter(submenuItem->rect)) }); + ASSERT_EQ(frame.request.popupRequests.size(), 2u); + + model.resolvedMenuModel.menus[0].items[0].children.clear(); + frame = UpdateUIEditorShellInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + {}); + + EXPECT_TRUE(frame.result.consumed); + EXPECT_TRUE(frame.result.menuMutation.changed); + EXPECT_EQ(frame.result.menuMutation.dismissReason, UIPopupDismissReason::Programmatic); + EXPECT_FALSE(state.menuSession.HasOpenMenu()); + EXPECT_TRUE(frame.request.popupRequests.empty()); +}