Stabilize XCEditor shell foundation widgets

This commit is contained in:
2026-04-07 01:42:02 +08:00
parent 998df9013a
commit 3b2a05a098
20 changed files with 4118 additions and 23 deletions

View File

@@ -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

View 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);
}

View 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

View 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