Stabilize XCEditor shell foundation widgets
This commit is contained in:
@@ -5,9 +5,12 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
|
||||
test_ui_editor_command_dispatcher.cpp
|
||||
test_ui_editor_command_registry.cpp
|
||||
test_ui_editor_menu_model.cpp
|
||||
test_ui_editor_menu_session.cpp
|
||||
test_ui_editor_panel_registry.cpp
|
||||
test_ui_editor_collection_primitives.cpp
|
||||
test_ui_editor_panel_chrome.cpp
|
||||
test_ui_editor_panel_frame.cpp
|
||||
test_ui_editor_tab_strip.cpp
|
||||
test_ui_editor_shortcut_manager.cpp
|
||||
test_ui_editor_workspace_controller.cpp
|
||||
test_ui_editor_workspace_layout_persistence.cpp
|
||||
|
||||
274
tests/UI/Editor/unit/test_ui_editor_menu_session.cpp
Normal file
274
tests/UI/Editor/unit/test_ui_editor_menu_session.cpp
Normal file
@@ -0,0 +1,274 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Core/UIEditorMenuSession.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIInputPath;
|
||||
using XCEngine::UI::Widgets::UIPopupDismissReason;
|
||||
using XCEngine::UI::Widgets::UIPopupOverlayEntry;
|
||||
using XCEngine::UI::Widgets::UIPopupPlacement;
|
||||
using XCEngine::UI::Editor::UIEditorMenuSession;
|
||||
|
||||
UIPopupOverlayEntry MakePopup(
|
||||
const char* popupId,
|
||||
const char* parentPopupId,
|
||||
UIInputPath surfacePath,
|
||||
UIInputPath anchorPath = {},
|
||||
UIPopupPlacement placement = UIPopupPlacement::BottomStart) {
|
||||
UIPopupOverlayEntry entry = {};
|
||||
entry.popupId = popupId;
|
||||
entry.parentPopupId = parentPopupId;
|
||||
entry.surfacePath = std::move(surfacePath);
|
||||
entry.anchorPath = std::move(anchorPath);
|
||||
entry.placement = placement;
|
||||
return entry;
|
||||
}
|
||||
|
||||
void ExpectClosedIds(
|
||||
const std::vector<std::string>& actual,
|
||||
std::initializer_list<const char*> expected) {
|
||||
ASSERT_EQ(actual.size(), expected.size());
|
||||
|
||||
std::size_t index = 0u;
|
||||
for (const char* popupId : expected) {
|
||||
EXPECT_EQ(actual[index], popupId);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
|
||||
void ExpectOpenSubmenuIds(
|
||||
const UIEditorMenuSession& session,
|
||||
std::initializer_list<const char*> expected) {
|
||||
const auto& actual = session.GetOpenSubmenuItemIds();
|
||||
ASSERT_EQ(actual.size(), expected.size());
|
||||
|
||||
std::size_t index = 0u;
|
||||
for (const char* itemId : expected) {
|
||||
EXPECT_EQ(actual[index], itemId);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorMenuSessionTest, OpenMenuBarRootTracksActiveMenuAndRootPopup) {
|
||||
UIEditorMenuSession session = {};
|
||||
|
||||
const auto result = session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}));
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.openRootMenuId, "file");
|
||||
EXPECT_EQ(result.openedPopupId, "menu.file.root");
|
||||
EXPECT_TRUE(result.closedPopupIds.empty());
|
||||
EXPECT_TRUE(session.IsMenuOpen("file"));
|
||||
EXPECT_TRUE(session.IsPopupOpen("menu.file.root"));
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 1u);
|
||||
ASSERT_EQ(session.GetPopupStates().size(), 1u);
|
||||
EXPECT_TRUE(session.GetPopupStates().front().IsRootPopup());
|
||||
EXPECT_TRUE(session.GetOpenSubmenuItemIds().empty());
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, HoverMenuBarRootReplacesOpenRootAndClearsSubmenuPath) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
const auto result = session.HoverMenuBarRoot(
|
||||
"window",
|
||||
MakePopup("menu.window.root", "", UIInputPath{300u, 310u}, UIInputPath{3u, 4u}));
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.openRootMenuId, "window");
|
||||
EXPECT_EQ(result.dismissReason, UIPopupDismissReason::Programmatic);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.root", "menu.file.layout"});
|
||||
EXPECT_TRUE(session.IsMenuOpen("window"));
|
||||
EXPECT_FALSE(session.IsMenuOpen("file"));
|
||||
EXPECT_TRUE(session.GetOpenSubmenuItemIds().empty());
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 1u);
|
||||
EXPECT_EQ(session.GetPopupOverlayModel().GetRootPopup()->popupId, "menu.window.root");
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, HoverSubmenuSwitchesSiblingBranchAndTruncatesDescendants) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"advanced",
|
||||
MakePopup(
|
||||
"menu.file.advanced",
|
||||
"menu.file.layout",
|
||||
UIInputPath{300u, 310u},
|
||||
UIInputPath{200u, 210u, 220u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
const auto result = session.HoverSubmenu(
|
||||
"export",
|
||||
MakePopup(
|
||||
"menu.file.export",
|
||||
"menu.file.root",
|
||||
UIInputPath{400u, 410u},
|
||||
UIInputPath{100u, 110u, 130u},
|
||||
UIPopupPlacement::RightStart));
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.layout", "menu.file.advanced"});
|
||||
EXPECT_EQ(result.openedPopupId, "menu.file.export");
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 2u);
|
||||
ExpectOpenSubmenuIds(session, {"export"});
|
||||
ASSERT_NE(session.FindPopupState("menu.file.export"), nullptr);
|
||||
EXPECT_EQ(session.FindPopupState("menu.file.export")->itemId, "export");
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, HoveringAlreadyOpenSubmenuKeepsExistingDescendants) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"advanced",
|
||||
MakePopup(
|
||||
"menu.file.advanced",
|
||||
"menu.file.layout",
|
||||
UIInputPath{300u, 310u},
|
||||
UIInputPath{200u, 210u, 220u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
const auto result = session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart));
|
||||
|
||||
EXPECT_FALSE(result.changed);
|
||||
EXPECT_EQ(result.openRootMenuId, "file");
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 3u);
|
||||
ExpectOpenSubmenuIds(session, {"layout", "advanced"});
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, PointerDismissInsideRootClosesOnlyDescendantSubmenus) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
const auto result = session.DismissFromPointerDown(UIInputPath{100u, 110u, 130u});
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.dismissReason, UIPopupDismissReason::PointerOutside);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.layout"});
|
||||
EXPECT_EQ(result.openRootMenuId, "file");
|
||||
EXPECT_TRUE(session.IsMenuOpen("file"));
|
||||
EXPECT_TRUE(session.GetOpenSubmenuItemIds().empty());
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, PointerDismissOutsideAllClosesEntireMenuChain) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
const auto result = session.DismissFromPointerDown(UIInputPath{999u, 1000u});
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.dismissReason, UIPopupDismissReason::PointerOutside);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.root", "menu.file.layout"});
|
||||
EXPECT_FALSE(result.HasOpenMenu());
|
||||
EXPECT_FALSE(session.HasOpenMenu());
|
||||
EXPECT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 0u);
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuSessionTest, EscapeDismissClosesTopmostPopupBeforeRoot) {
|
||||
UIEditorMenuSession session = {};
|
||||
ASSERT_TRUE(session.OpenMenuBarRoot(
|
||||
"file",
|
||||
MakePopup("menu.file.root", "", UIInputPath{100u, 110u}, UIInputPath{1u, 2u}))
|
||||
.changed);
|
||||
ASSERT_TRUE(session.HoverSubmenu(
|
||||
"layout",
|
||||
MakePopup(
|
||||
"menu.file.layout",
|
||||
"menu.file.root",
|
||||
UIInputPath{200u, 210u},
|
||||
UIInputPath{100u, 110u, 120u},
|
||||
UIPopupPlacement::RightStart))
|
||||
.changed);
|
||||
|
||||
auto result = session.DismissFromEscape();
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.dismissReason, UIPopupDismissReason::EscapeKey);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.layout"});
|
||||
EXPECT_EQ(result.openRootMenuId, "file");
|
||||
ASSERT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 1u);
|
||||
|
||||
result = session.DismissFromEscape();
|
||||
|
||||
EXPECT_TRUE(result.changed);
|
||||
EXPECT_EQ(result.dismissReason, UIPopupDismissReason::EscapeKey);
|
||||
ExpectClosedIds(result.closedPopupIds, {"menu.file.root"});
|
||||
EXPECT_FALSE(session.HasOpenMenu());
|
||||
EXPECT_EQ(session.GetPopupOverlayModel().GetPopupCount(), 0u);
|
||||
}
|
||||
165
tests/UI/Editor/unit/test_ui_editor_panel_frame.cpp
Normal file
165
tests/UI/Editor/unit/test_ui_editor_panel_frame.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Widgets/UIEditorPanelFrame.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIColor;
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorPanelFrameBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorPanelFrameForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorPanelFrameLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorPanelFrame;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorPanelFrameAction;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorPanelFrameBorderColor;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorPanelFrameBorderThickness;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameAction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameHitTarget;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFramePalette;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameState;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameText;
|
||||
|
||||
void ExpectColorEq(
|
||||
const UIColor& actual,
|
||||
const UIColor& expected) {
|
||||
EXPECT_FLOAT_EQ(actual.r, expected.r);
|
||||
EXPECT_FLOAT_EQ(actual.g, expected.g);
|
||||
EXPECT_FLOAT_EQ(actual.b, expected.b);
|
||||
EXPECT_FLOAT_EQ(actual.a, expected.a);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelFrameTest, LayoutBuildsHeaderBodyFooterAndActionRectsFromState) {
|
||||
UIEditorPanelFrameState state = {};
|
||||
state.active = true;
|
||||
state.pinned = true;
|
||||
state.closable = true;
|
||||
state.pinnable = true;
|
||||
state.showFooter = true;
|
||||
|
||||
const UIEditorPanelFrameLayout layout =
|
||||
BuildUIEditorPanelFrameLayout(UIRect(10.0f, 20.0f, 300.0f, 200.0f), state);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.y, 20.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.width, 300.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.height, 36.0f);
|
||||
|
||||
EXPECT_TRUE(layout.hasFooter);
|
||||
EXPECT_FLOAT_EQ(layout.footerRect.y, 196.0f);
|
||||
EXPECT_FLOAT_EQ(layout.footerRect.height, 24.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.bodyRect.x, 22.0f);
|
||||
EXPECT_FLOAT_EQ(layout.bodyRect.y, 68.0f);
|
||||
EXPECT_FLOAT_EQ(layout.bodyRect.width, 276.0f);
|
||||
EXPECT_FLOAT_EQ(layout.bodyRect.height, 116.0f);
|
||||
|
||||
EXPECT_TRUE(layout.showPinButton);
|
||||
EXPECT_TRUE(layout.showCloseButton);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRect.x, 286.0f);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRect.y, 32.0f);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRect.width, 12.0f);
|
||||
EXPECT_FLOAT_EQ(layout.pinButtonRect.x, 268.0f);
|
||||
EXPECT_FLOAT_EQ(layout.pinButtonRect.y, 32.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelFrameTest, BorderPolicyUsesFocusBeforeActiveBeforeHover) {
|
||||
const UIEditorPanelFramePalette palette = {};
|
||||
|
||||
UIEditorPanelFrameState hoveredOnly = {};
|
||||
hoveredOnly.hovered = true;
|
||||
UIEditorPanelFrameState activeOnly = hoveredOnly;
|
||||
activeOnly.active = true;
|
||||
UIEditorPanelFrameState focusedState = activeOnly;
|
||||
focusedState.focused = true;
|
||||
|
||||
ExpectColorEq(
|
||||
ResolveUIEditorPanelFrameBorderColor(hoveredOnly, palette),
|
||||
palette.hoveredBorderColor);
|
||||
ExpectColorEq(
|
||||
ResolveUIEditorPanelFrameBorderColor(activeOnly, palette),
|
||||
palette.activeBorderColor);
|
||||
ExpectColorEq(
|
||||
ResolveUIEditorPanelFrameBorderColor(focusedState, palette),
|
||||
palette.focusedBorderColor);
|
||||
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorPanelFrameBorderThickness(UIEditorPanelFrameState{}), 1.0f);
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorPanelFrameBorderThickness(hoveredOnly), 1.25f);
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorPanelFrameBorderThickness(activeOnly), 1.5f);
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorPanelFrameBorderThickness(focusedState), 2.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelFrameTest, HitTestReturnsActionAndRegionTargetsFromLayout) {
|
||||
UIEditorPanelFrameState state = {};
|
||||
state.closable = true;
|
||||
state.pinnable = true;
|
||||
state.showFooter = true;
|
||||
|
||||
const UIEditorPanelFrameLayout layout =
|
||||
BuildUIEditorPanelFrameLayout(UIRect(10.0f, 20.0f, 300.0f, 200.0f), state);
|
||||
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorPanelFrameAction(layout, state, UIPoint(269.0f, 35.0f)),
|
||||
UIEditorPanelFrameAction::Pin);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorPanelFrameAction(layout, state, UIPoint(287.0f, 35.0f)),
|
||||
UIEditorPanelFrameAction::Close);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorPanelFrame(layout, state, UIPoint(40.0f, 35.0f)),
|
||||
UIEditorPanelFrameHitTarget::Header);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorPanelFrame(layout, state, UIPoint(40.0f, 90.0f)),
|
||||
UIEditorPanelFrameHitTarget::Body);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorPanelFrame(layout, state, UIPoint(40.0f, 205.0f)),
|
||||
UIEditorPanelFrameHitTarget::Footer);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelFrameTest, BackgroundAndForegroundEmitExpectedChromeAndActionCommands) {
|
||||
UIEditorPanelFrameState state = {};
|
||||
state.active = true;
|
||||
state.focused = true;
|
||||
state.pinned = true;
|
||||
state.closable = true;
|
||||
state.pinnable = true;
|
||||
state.showFooter = true;
|
||||
state.pinHovered = true;
|
||||
|
||||
const UIEditorPanelFramePalette palette = {};
|
||||
const UIEditorPanelFrameText text{
|
||||
"Inspector",
|
||||
"PanelFrame chrome contract",
|
||||
"focused | pinned | footer visible"
|
||||
};
|
||||
const UIEditorPanelFrameLayout layout =
|
||||
BuildUIEditorPanelFrameLayout(UIRect(20.0f, 30.0f, 320.0f, 220.0f), state);
|
||||
|
||||
UIDrawList background("PanelFrameBackground");
|
||||
AppendUIEditorPanelFrameBackground(background, layout, state, palette);
|
||||
|
||||
ASSERT_EQ(background.GetCommandCount(), 4u);
|
||||
const auto& backgroundCommands = background.GetCommands();
|
||||
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[1].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[2].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[3].type, UIDrawCommandType::FilledRect);
|
||||
ExpectColorEq(backgroundCommands[1].color, palette.focusedBorderColor);
|
||||
|
||||
UIDrawList foreground("PanelFrameForeground");
|
||||
AppendUIEditorPanelFrameForeground(foreground, layout, state, text, palette);
|
||||
|
||||
ASSERT_EQ(foreground.GetCommandCount(), 9u);
|
||||
const auto& foregroundCommands = foreground.GetCommands();
|
||||
EXPECT_EQ(foregroundCommands[0].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[0].text, "Inspector");
|
||||
EXPECT_EQ(foregroundCommands[1].text, "PanelFrame chrome contract");
|
||||
EXPECT_EQ(foregroundCommands[2].text, "focused | pinned | footer visible");
|
||||
EXPECT_EQ(foregroundCommands[5].text, "P");
|
||||
EXPECT_EQ(foregroundCommands[8].text, "X");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
178
tests/UI/Editor/unit/test_ui_editor_tab_strip.cpp
Normal file
178
tests/UI/Editor/unit/test_ui_editor_tab_strip.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Widgets/UIEditorTabStrip.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorTabStripBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorTabStripForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorTabStripLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTabStrip;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripDesiredHeaderLabelWidth;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripSelectedIndex;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripSelectedIndexAfterClose;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripItem;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripMetrics;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTabStripState;
|
||||
|
||||
TEST(UIEditorTabStripTest, DesiredHeaderWidthReservesCloseButtonBudget) {
|
||||
UIEditorTabStripMetrics metrics = {};
|
||||
metrics.layoutMetrics.tabHorizontalPadding = 10.0f;
|
||||
metrics.estimatedGlyphWidth = 8.0f;
|
||||
metrics.closeButtonExtent = 14.0f;
|
||||
metrics.closeButtonGap = 6.0f;
|
||||
metrics.closeInsetRight = 14.0f;
|
||||
metrics.labelInsetX = 12.0f;
|
||||
|
||||
const float closableWidth = ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||
UIEditorTabStripItem{ "doc-a", "ABCD", true, 0.0f },
|
||||
metrics);
|
||||
const float fixedWidth = ResolveUIEditorTabStripDesiredHeaderLabelWidth(
|
||||
UIEditorTabStripItem{ "doc-b", "Ignored", false, 42.0f },
|
||||
metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(closableWidth, 58.0f);
|
||||
EXPECT_FLOAT_EQ(fixedWidth, 44.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, SelectedIndexResolvesByTabIdAndFallsBackToValidRange) {
|
||||
const std::vector<UIEditorTabStripItem> items = {
|
||||
{ "doc-a", "Document A", true, 0.0f },
|
||||
{ "doc-b", "Document B", true, 0.0f },
|
||||
{ "doc-c", "Document C", false, 0.0f }
|
||||
};
|
||||
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndex(items, "doc-b"), 1u);
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndex(items, "missing", 2u), 2u);
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndex(items, "missing"), 0u);
|
||||
EXPECT_EQ(
|
||||
ResolveUIEditorTabStripSelectedIndex({}, "missing"),
|
||||
UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, ClosingTabsResolvesSelectionFallbackFromClosedIndex) {
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndexAfterClose(1u, 1u, 3u), 1u);
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndexAfterClose(2u, 2u, 3u), 1u);
|
||||
EXPECT_EQ(ResolveUIEditorTabStripSelectedIndexAfterClose(2u, 0u, 3u), 1u);
|
||||
EXPECT_EQ(
|
||||
ResolveUIEditorTabStripSelectedIndexAfterClose(0u, 0u, 1u),
|
||||
UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, LayoutUsesCoreTabArrangementAndBuildsCloseRects) {
|
||||
UIEditorTabStripMetrics metrics = {};
|
||||
metrics.layoutMetrics.headerHeight = 30.0f;
|
||||
metrics.layoutMetrics.tabMinWidth = 80.0f;
|
||||
metrics.layoutMetrics.tabHorizontalPadding = 12.0f;
|
||||
metrics.layoutMetrics.tabGap = 4.0f;
|
||||
metrics.closeButtonExtent = 12.0f;
|
||||
metrics.closeButtonGap = 6.0f;
|
||||
metrics.closeInsetRight = 12.0f;
|
||||
metrics.labelInsetX = 12.0f;
|
||||
|
||||
const std::vector<UIEditorTabStripItem> items = {
|
||||
{ "doc-a", "Document A", true, 48.0f },
|
||||
{ "doc-b", "Document B", false, 40.0f }
|
||||
};
|
||||
|
||||
UIEditorTabStripState state = {};
|
||||
state.selectedIndex = 0u;
|
||||
|
||||
const UIEditorTabStripLayout layout =
|
||||
BuildUIEditorTabStripLayout(UIRect(10.0f, 20.0f, 260.0f, 180.0f), items, state, metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.y, 20.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.width, 260.0f);
|
||||
EXPECT_FLOAT_EQ(layout.headerRect.height, 30.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.y, 50.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.width, 260.0f);
|
||||
EXPECT_FLOAT_EQ(layout.contentRect.height, 150.0f);
|
||||
|
||||
ASSERT_EQ(layout.tabHeaderRects.size(), 2u);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[0].x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[0].width, 90.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[1].x, 104.0f);
|
||||
EXPECT_FLOAT_EQ(layout.tabHeaderRects[1].width, 80.0f);
|
||||
|
||||
ASSERT_EQ(layout.closeButtonRects.size(), 2u);
|
||||
EXPECT_TRUE(layout.showCloseButtons[0]);
|
||||
EXPECT_FALSE(layout.showCloseButtons[1]);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRects[0].x, 76.0f);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRects[0].y, 29.0f);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRects[0].width, 12.0f);
|
||||
EXPECT_FLOAT_EQ(layout.closeButtonRects[0].height, 12.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, HitTestPrioritizesCloseButtonThenTabThenContent) {
|
||||
const std::vector<UIEditorTabStripItem> items = {
|
||||
{ "doc-a", "Document A", true, 48.0f },
|
||||
{ "doc-b", "Document B", false, 40.0f }
|
||||
};
|
||||
|
||||
UIEditorTabStripState state = {};
|
||||
state.selectedIndex = 0u;
|
||||
const UIEditorTabStripLayout layout =
|
||||
BuildUIEditorTabStripLayout(UIRect(10.0f, 20.0f, 260.0f, 180.0f), items, state);
|
||||
|
||||
const auto closeHit = HitTestUIEditorTabStrip(layout, state, UIPoint(85.0f, 34.0f));
|
||||
EXPECT_EQ(closeHit.kind, UIEditorTabStripHitTargetKind::CloseButton);
|
||||
EXPECT_EQ(closeHit.index, 0u);
|
||||
|
||||
const auto tabHit = HitTestUIEditorTabStrip(layout, state, UIPoint(40.0f, 34.0f));
|
||||
EXPECT_EQ(tabHit.kind, UIEditorTabStripHitTargetKind::Tab);
|
||||
EXPECT_EQ(tabHit.index, 0u);
|
||||
|
||||
const auto contentHit = HitTestUIEditorTabStrip(layout, state, UIPoint(40.0f, 70.0f));
|
||||
EXPECT_EQ(contentHit.kind, UIEditorTabStripHitTargetKind::Content);
|
||||
EXPECT_EQ(contentHit.index, UIEditorTabStripInvalidIndex);
|
||||
}
|
||||
|
||||
TEST(UIEditorTabStripTest, BackgroundAndForegroundEmitStableChromeCommands) {
|
||||
const std::vector<UIEditorTabStripItem> items = {
|
||||
{ "doc-a", "Document A", true, 48.0f },
|
||||
{ "doc-b", "Document B", false, 40.0f }
|
||||
};
|
||||
|
||||
UIEditorTabStripState state = {};
|
||||
state.selectedIndex = 0u;
|
||||
state.hoveredIndex = 1u;
|
||||
state.closeHoveredIndex = 0u;
|
||||
state.focused = true;
|
||||
|
||||
const UIEditorTabStripLayout layout =
|
||||
BuildUIEditorTabStripLayout(UIRect(10.0f, 20.0f, 260.0f, 180.0f), items, state);
|
||||
|
||||
UIDrawList background("TabStripBackground");
|
||||
AppendUIEditorTabStripBackground(background, layout, state);
|
||||
ASSERT_EQ(background.GetCommandCount(), 8u);
|
||||
const auto& backgroundCommands = background.GetCommands();
|
||||
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[3].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[4].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[7].type, UIDrawCommandType::RectOutline);
|
||||
|
||||
UIDrawList foreground("TabStripForeground");
|
||||
AppendUIEditorTabStripForeground(foreground, layout, items, state);
|
||||
ASSERT_EQ(foreground.GetCommandCount(), 9u);
|
||||
const auto& foregroundCommands = foreground.GetCommands();
|
||||
EXPECT_EQ(foregroundCommands[0].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_EQ(foregroundCommands[1].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[1].text, "Document A");
|
||||
EXPECT_EQ(foregroundCommands[5].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[5].text, "X");
|
||||
EXPECT_EQ(foregroundCommands[7].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[7].text, "Document B");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user