Build XCEditor viewport slot shell foundation

This commit is contained in:
2026-04-07 04:23:33 +08:00
parent 8eeb7af56e
commit 7f0d1f0b08
11 changed files with 1807 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_panel_frame.cpp
test_ui_editor_status_bar.cpp
test_ui_editor_tab_strip.cpp
test_ui_editor_viewport_slot.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,281 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEditor/Widgets/UIEditorViewportSlot.h>
namespace {
using XCEngine::UI::UIColor;
using XCEngine::UI::UIDrawCommand;
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::UISize;
using XCEngine::UI::UITextureHandle;
using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotForeground;
using XCEngine::UI::Editor::Widgets::BuildUIEditorViewportSlotLayout;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorViewportSlot;
using XCEngine::UI::Editor::Widgets::ResolveUIEditorViewportSlotDesiredToolWidth;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotChrome;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotFrame;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotInvalidIndex;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotLayout;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotPalette;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotState;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolItem;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot;
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);
}
bool RectEq(const UIRect& actual, const UIRect& expected) {
return actual.x == expected.x &&
actual.y == expected.y &&
actual.width == expected.width &&
actual.height == expected.height;
}
const UIDrawCommand* FindCommand(
const UIDrawList& drawList,
UIDrawCommandType type,
const UIRect& rect) {
for (const UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == type && RectEq(command.rect, rect)) {
return &command;
}
}
return nullptr;
}
bool ContainsText(const UIDrawList& drawList, std::string_view text) {
for (const UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == UIDrawCommandType::Text && command.text == text) {
return true;
}
}
return false;
}
std::vector<UIEditorViewportSlotToolItem> BuildToolItems() {
return {
{ "mode", "Perspective", UIEditorViewportSlotToolSlot::Leading, true, true, 96.0f },
{ "lit", "Lit", UIEditorViewportSlotToolSlot::Trailing, true, false, 48.0f },
{ "gizmos", "Gizmos", UIEditorViewportSlotToolSlot::Trailing, true, false, 72.0f }
};
}
std::vector<UIEditorStatusBarSegment> BuildStatusSegments() {
return {
{ "scene", "Scene", UIEditorStatusBarSlot::Leading, {}, true, true, 72.0f },
{ "resolution", "1280x720", UIEditorStatusBarSlot::Leading, {}, true, false, 92.0f },
{ "frame", "16.7 ms", UIEditorStatusBarSlot::Trailing, {}, true, false, 72.0f }
};
}
TEST(UIEditorViewportSlotTest, DesiredToolWidthUsesExplicitValueBeforeEstimatedLabelWidth) {
UIEditorViewportSlotToolItem explicitWidth = {};
explicitWidth.label = "Scene";
explicitWidth.desiredWidth = 88.0f;
UIEditorViewportSlotToolItem inferredWidth = {};
inferredWidth.label = "Scene";
EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(explicitWidth), 88.0f);
EXPECT_FLOAT_EQ(ResolveUIEditorViewportSlotDesiredToolWidth(inferredWidth), 55.0f);
}
TEST(UIEditorViewportSlotTest, LayoutBuildsTopBarSurfaceBottomBarAndAspectFittedTexture) {
UIEditorViewportSlotChrome chrome = {};
chrome.topBarHeight = 40.0f;
chrome.bottomBarHeight = 28.0f;
UIEditorViewportSlotFrame frame = {};
frame.hasTexture = true;
frame.presentedSize = UISize(1600.0f, 900.0f);
const UIEditorViewportSlotLayout layout =
BuildUIEditorViewportSlotLayout(
UIRect(10.0f, 20.0f, 800.0f, 600.0f),
chrome,
frame,
BuildToolItems(),
BuildStatusSegments());
EXPECT_TRUE(layout.hasTopBar);
EXPECT_TRUE(layout.hasBottomBar);
EXPECT_FLOAT_EQ(layout.topBarRect.y, 20.0f);
EXPECT_FLOAT_EQ(layout.topBarRect.height, 40.0f);
EXPECT_FLOAT_EQ(layout.bottomBarRect.y, 592.0f);
EXPECT_FLOAT_EQ(layout.bottomBarRect.height, 28.0f);
EXPECT_FLOAT_EQ(layout.surfaceRect.y, 60.0f);
EXPECT_FLOAT_EQ(layout.surfaceRect.height, 532.0f);
EXPECT_FLOAT_EQ(layout.requestedSurfaceSize.width, 800.0f);
EXPECT_FLOAT_EQ(layout.requestedSurfaceSize.height, 532.0f);
EXPECT_FLOAT_EQ(layout.textureRect.x, 10.0f);
EXPECT_FLOAT_EQ(layout.textureRect.y, 101.0f);
EXPECT_FLOAT_EQ(layout.textureRect.width, 800.0f);
EXPECT_FLOAT_EQ(layout.textureRect.height, 450.0f);
}
TEST(UIEditorViewportSlotTest, ToolItemsAlignToEdgesAndTitleRectClampsBetweenToolBands) {
UIEditorViewportSlotChrome chrome = {};
chrome.topBarHeight = 40.0f;
chrome.bottomBarHeight = 0.0f;
chrome.showBottomBar = false;
const UIEditorViewportSlotLayout layout =
BuildUIEditorViewportSlotLayout(
UIRect(0.0f, 0.0f, 900.0f, 520.0f),
chrome,
UIEditorViewportSlotFrame{},
BuildToolItems(),
{});
EXPECT_FLOAT_EQ(layout.toolItemRects[0].x, 12.0f);
EXPECT_FLOAT_EQ(layout.toolItemRects[0].width, 96.0f);
EXPECT_FLOAT_EQ(layout.toolItemRects[1].x, 762.0f);
EXPECT_FLOAT_EQ(layout.toolItemRects[2].x, 816.0f);
EXPECT_FLOAT_EQ(layout.titleRect.x, 118.0f);
EXPECT_FLOAT_EQ(layout.titleRect.width, 634.0f);
}
TEST(UIEditorViewportSlotTest, HitTestPrioritizesToolThenStatusThenSurface) {
UIEditorViewportSlotChrome chrome = {};
chrome.topBarHeight = 40.0f;
chrome.bottomBarHeight = 28.0f;
const UIEditorViewportSlotLayout layout =
BuildUIEditorViewportSlotLayout(
UIRect(0.0f, 0.0f, 900.0f, 520.0f),
chrome,
UIEditorViewportSlotFrame{},
BuildToolItems(),
BuildStatusSegments());
auto hit = HitTestUIEditorViewportSlot(layout, UIPoint(30.0f, 16.0f));
EXPECT_EQ(hit.kind, UIEditorViewportSlotHitTargetKind::ToolItem);
EXPECT_EQ(hit.index, 0u);
hit = HitTestUIEditorViewportSlot(layout, UIPoint(22.0f, 505.0f));
EXPECT_EQ(hit.kind, UIEditorViewportSlotHitTargetKind::StatusSegment);
EXPECT_EQ(hit.index, 0u);
hit = HitTestUIEditorViewportSlot(layout, UIPoint(450.0f, 240.0f));
EXPECT_EQ(hit.kind, UIEditorViewportSlotHitTargetKind::Surface);
EXPECT_EQ(hit.index, UIEditorViewportSlotInvalidIndex);
}
TEST(UIEditorViewportSlotTest, BackgroundAndForegroundEmitChromeAndImageBranchCommands) {
UIEditorViewportSlotChrome chrome = {};
chrome.title = "Scene View";
chrome.subtitle = "ViewportSlot shell";
UIEditorViewportSlotFrame frame = {};
frame.hasTexture = true;
frame.texture = UITextureHandle{ 1u, 1280u, 720u };
frame.presentedSize = UISize(1280.0f, 720.0f);
UIEditorViewportSlotState state = {};
state.focused = true;
state.surfaceHovered = true;
state.surfaceActive = true;
state.inputCaptured = true;
state.hoveredToolIndex = 0u;
state.activeToolIndex = 1u;
state.statusBarState.focused = true;
const auto toolItems = BuildToolItems();
const auto statusSegments = BuildStatusSegments();
const UIEditorViewportSlotLayout layout =
BuildUIEditorViewportSlotLayout(
UIRect(12.0f, 16.0f, 900.0f, 520.0f),
chrome,
frame,
toolItems,
statusSegments);
const UIEditorViewportSlotPalette palette = {};
UIDrawList background("ViewportSlotBackground");
AppendUIEditorViewportSlotBackground(
background,
layout,
toolItems,
statusSegments,
state,
palette);
const UIDrawCommand* surfaceBorder =
FindCommand(background, UIDrawCommandType::RectOutline, layout.inputRect);
ASSERT_NE(surfaceBorder, nullptr);
ExpectColorEq(surfaceBorder->color, palette.surfaceCapturedBorderColor);
UIDrawList foreground("ViewportSlotForeground");
AppendUIEditorViewportSlotForeground(
foreground,
layout,
chrome,
frame,
toolItems,
statusSegments,
state,
palette);
EXPECT_TRUE(ContainsText(foreground, "Scene View"));
EXPECT_TRUE(ContainsText(foreground, "Perspective"));
EXPECT_TRUE(ContainsText(foreground, "Texture 1280x720"));
bool foundImage = false;
for (const UIDrawCommand& command : foreground.GetCommands()) {
if (command.type == UIDrawCommandType::Image &&
RectEq(command.rect, layout.textureRect)) {
foundImage = true;
break;
}
}
EXPECT_TRUE(foundImage);
}
TEST(UIEditorViewportSlotTest, ForegroundFallsBackToStatusTextWhenTextureIsUnavailable) {
UIEditorViewportSlotChrome chrome = {};
chrome.title = "Game View";
UIEditorViewportSlotFrame frame = {};
frame.statusText = "Viewport is waiting for frame";
const auto toolItems = BuildToolItems();
const auto statusSegments = BuildStatusSegments();
const UIEditorViewportSlotLayout layout =
BuildUIEditorViewportSlotLayout(
UIRect(0.0f, 0.0f, 720.0f, 420.0f),
chrome,
frame,
toolItems,
statusSegments);
UIDrawList foreground("ViewportSlotForegroundFallback");
AppendUIEditorViewportSlotForeground(
foreground,
layout,
chrome,
frame,
toolItems,
statusSegments,
UIEditorViewportSlotState{});
EXPECT_TRUE(ContainsText(foreground, "Viewport is waiting for frame"));
EXPECT_FALSE(ContainsText(foreground, "Texture 1280x720"));
}
} // namespace