feat(xcui): add tab strip and workspace compose foundations

This commit is contained in:
2026-04-06 04:27:54 +08:00
parent 3540dbc94d
commit b14a4fb7bb
27 changed files with 2075 additions and 41 deletions

View File

@@ -2,6 +2,8 @@ set(CORE_UI_TEST_SOURCES
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_shortcut_scope.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_splitter_layout.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_splitter_interaction.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_tab_strip_layout.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_tab_strip_model.cpp
# Migration bridge: legacy XCUI unit coverage still lives under tests/Core/UI
# until it is moved into tests/UI/Core/unit without changing behavior.
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_core.cpp

View File

@@ -0,0 +1,72 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Layout/UITabStripLayout.h>
namespace {
using XCEngine::UI::UIRect;
using XCEngine::UI::UISize;
using XCEngine::UI::Layout::ArrangeUITabStrip;
using XCEngine::UI::Layout::MeasureUITabStrip;
using XCEngine::UI::Layout::MeasureUITabStripHeaderWidth;
using XCEngine::UI::Layout::UITabStripMeasureItem;
using XCEngine::UI::Layout::UITabStripMetrics;
void ExpectRect(
const UIRect& rect,
float x,
float y,
float width,
float height) {
EXPECT_FLOAT_EQ(rect.x, x);
EXPECT_FLOAT_EQ(rect.y, y);
EXPECT_FLOAT_EQ(rect.width, width);
EXPECT_FLOAT_EQ(rect.height, height);
}
} // namespace
TEST(UITabStripLayoutTest, MeasureUsesTallestContentAndWidestHeaderBudget) {
const UITabStripMetrics metrics = { 32.0f, 80.0f, 10.0f, 2.0f };
const auto measured = MeasureUITabStrip(
{
UITabStripMeasureItem{ 36.0f, UISize(220.0f, 140.0f), UISize(120.0f, 80.0f) },
UITabStripMeasureItem{ 96.0f, UISize(180.0f, 200.0f), UISize(160.0f, 90.0f) }
},
metrics);
const float desiredHeaderWidth =
MeasureUITabStripHeaderWidth(36.0f, metrics) +
MeasureUITabStripHeaderWidth(96.0f, metrics) +
metrics.tabGap;
const float minimumHeaderWidth = metrics.tabMinWidth * 2.0f + metrics.tabGap;
EXPECT_FLOAT_EQ(measured.desiredSize.width, 220.0f);
EXPECT_FLOAT_EQ(measured.desiredSize.height, metrics.headerHeight + 200.0f);
EXPECT_FLOAT_EQ(measured.minimumSize.width, minimumHeaderWidth);
EXPECT_FLOAT_EQ(measured.minimumSize.height, metrics.headerHeight + 90.0f);
}
TEST(UITabStripLayoutTest, MeasureWithoutItemsReturnsZeroSize) {
const auto measured = MeasureUITabStrip({}, UITabStripMetrics{});
EXPECT_FLOAT_EQ(measured.desiredSize.width, 0.0f);
EXPECT_FLOAT_EQ(measured.desiredSize.height, 0.0f);
EXPECT_FLOAT_EQ(measured.minimumSize.width, 0.0f);
EXPECT_FLOAT_EQ(measured.minimumSize.height, 0.0f);
}
TEST(UITabStripLayoutTest, ArrangeScalesHeadersToAvailableWidthAndReservesContentArea) {
const UITabStripMetrics metrics = { 30.0f, 72.0f, 12.0f, 4.0f };
const auto arranged = ArrangeUITabStrip(
UIRect(10.0f, 20.0f, 180.0f, 120.0f),
{ 120.0f, 100.0f },
metrics);
ExpectRect(arranged.headerRect, 10.0f, 20.0f, 180.0f, 30.0f);
ExpectRect(arranged.contentRect, 10.0f, 50.0f, 180.0f, 90.0f);
ASSERT_EQ(arranged.tabHeaderRects.size(), 2u);
EXPECT_NEAR(arranged.tabHeaderRects[0].width + arranged.tabHeaderRects[1].width, 176.0f, 0.001f);
ExpectRect(arranged.tabHeaderRects[0], 10.0f, 20.0f, 96.0f, 30.0f);
ExpectRect(arranged.tabHeaderRects[1], 110.0f, 20.0f, 80.0f, 30.0f);
}

View File

@@ -0,0 +1,53 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Widgets/UITabStripModel.h>
namespace {
using XCEngine::UI::Widgets::UITabStripModel;
} // namespace
TEST(UITabStripModelTest, SetItemCountInitializesAndClampsSelection) {
UITabStripModel model = {};
EXPECT_TRUE(model.SetItemCount(3u));
EXPECT_TRUE(model.HasSelection());
EXPECT_EQ(model.GetSelectedIndex(), 0u);
EXPECT_TRUE(model.SetSelectedIndex(2u));
EXPECT_EQ(model.GetSelectedIndex(), 2u);
EXPECT_TRUE(model.SetItemCount(2u));
EXPECT_EQ(model.GetSelectedIndex(), 1u);
}
TEST(UITabStripModelTest, NextPreviousFirstAndLastStayWithinBounds) {
UITabStripModel model = {};
ASSERT_TRUE(model.SetItemCount(3u));
EXPECT_TRUE(model.SelectLast());
EXPECT_EQ(model.GetSelectedIndex(), 2u);
EXPECT_FALSE(model.SelectNext());
EXPECT_EQ(model.GetSelectedIndex(), 2u);
EXPECT_TRUE(model.SelectPrevious());
EXPECT_EQ(model.GetSelectedIndex(), 1u);
EXPECT_TRUE(model.SelectFirst());
EXPECT_EQ(model.GetSelectedIndex(), 0u);
EXPECT_FALSE(model.SelectPrevious());
EXPECT_EQ(model.GetSelectedIndex(), 0u);
}
TEST(UITabStripModelTest, RejectsOutOfRangeSelectionAndClearsWhenEmpty) {
UITabStripModel model = {};
ASSERT_TRUE(model.SetItemCount(2u));
EXPECT_FALSE(model.SetSelectedIndex(3u));
EXPECT_EQ(model.GetSelectedIndex(), 0u);
EXPECT_TRUE(model.SetItemCount(0u));
EXPECT_FALSE(model.HasSelection());
EXPECT_EQ(model.GetSelectedIndex(), UITabStripModel::InvalidIndex);
EXPECT_FALSE(model.SetSelectedIndex(0u));
}