Harden shell interaction modal recovery
This commit is contained in:
@@ -137,7 +137,10 @@ const std::vector<UIEditorResolvedMenuItem>* 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user