Add XCUI runtime screen layer and demo textarea
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
|
||||
set(UI_TEST_SOURCES
|
||||
test_ui_core.cpp
|
||||
test_layout_engine.cpp
|
||||
test_ui_runtime.cpp
|
||||
)
|
||||
|
||||
add_executable(core_ui_tests ${UI_TEST_SOURCES})
|
||||
|
||||
130
tests/Core/UI/test_layout_engine.cpp
Normal file
130
tests/Core/UI/test_layout_engine.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Layout/LayoutEngine.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::UISize;
|
||||
using XCEngine::UI::Layout::ArrangeOverlayLayout;
|
||||
using XCEngine::UI::Layout::ArrangeStackLayout;
|
||||
using XCEngine::UI::Layout::MeasureOverlayLayout;
|
||||
using XCEngine::UI::Layout::MeasureStackLayout;
|
||||
using XCEngine::UI::Layout::UILayoutAlignment;
|
||||
using XCEngine::UI::Layout::UILayoutAxis;
|
||||
using XCEngine::UI::Layout::UILayoutConstraints;
|
||||
using XCEngine::UI::Layout::UILayoutItem;
|
||||
using XCEngine::UI::Layout::UILayoutLength;
|
||||
using XCEngine::UI::Layout::UILayoutThickness;
|
||||
using XCEngine::UI::Layout::UIOverlayLayoutOptions;
|
||||
using XCEngine::UI::Layout::UIStackLayoutOptions;
|
||||
|
||||
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(UI_Layout, MeasureHorizontalStackAccumulatesSpacingPaddingAndCrossExtent) {
|
||||
UIStackLayoutOptions options = {};
|
||||
options.axis = UILayoutAxis::Horizontal;
|
||||
options.spacing = 5.0f;
|
||||
options.padding = UILayoutThickness::Symmetric(10.0f, 6.0f);
|
||||
|
||||
std::vector<UILayoutItem> items(2);
|
||||
items[0].desiredContentSize = UISize(40.0f, 20.0f);
|
||||
items[1].desiredContentSize = UISize(60.0f, 30.0f);
|
||||
|
||||
const auto result = MeasureStackLayout(options, items);
|
||||
|
||||
EXPECT_FLOAT_EQ(result.desiredSize.width, 125.0f);
|
||||
EXPECT_FLOAT_EQ(result.desiredSize.height, 42.0f);
|
||||
}
|
||||
|
||||
TEST(UI_Layout, ArrangeHorizontalStackDistributesRemainingSpaceToStretchChildren) {
|
||||
UIStackLayoutOptions options = {};
|
||||
options.axis = UILayoutAxis::Horizontal;
|
||||
options.spacing = 5.0f;
|
||||
options.padding = UILayoutThickness::Uniform(10.0f);
|
||||
|
||||
std::vector<UILayoutItem> items(3);
|
||||
items[0].width = UILayoutLength::Pixels(100.0f);
|
||||
items[0].desiredContentSize = UISize(10.0f, 20.0f);
|
||||
|
||||
items[1].width = UILayoutLength::Stretch(1.0f);
|
||||
items[1].desiredContentSize = UISize(30.0f, 20.0f);
|
||||
|
||||
items[2].width = UILayoutLength::Pixels(50.0f);
|
||||
items[2].desiredContentSize = UISize(10.0f, 20.0f);
|
||||
|
||||
const auto result = ArrangeStackLayout(options, items, UIRect(0.0f, 0.0f, 300.0f, 80.0f));
|
||||
|
||||
ExpectRect(result.children[0].arrangedRect, 10.0f, 10.0f, 100.0f, 20.0f);
|
||||
ExpectRect(result.children[1].arrangedRect, 115.0f, 10.0f, 120.0f, 20.0f);
|
||||
ExpectRect(result.children[2].arrangedRect, 240.0f, 10.0f, 50.0f, 20.0f);
|
||||
}
|
||||
|
||||
TEST(UI_Layout, ArrangeVerticalStackSupportsCrossAxisStretch) {
|
||||
UIStackLayoutOptions options = {};
|
||||
options.axis = UILayoutAxis::Vertical;
|
||||
options.spacing = 4.0f;
|
||||
options.padding = UILayoutThickness::Symmetric(8.0f, 6.0f);
|
||||
|
||||
std::vector<UILayoutItem> items(2);
|
||||
items[0].desiredContentSize = UISize(40.0f, 10.0f);
|
||||
items[0].horizontalAlignment = UILayoutAlignment::Stretch;
|
||||
items[1].desiredContentSize = UISize(60.0f, 20.0f);
|
||||
|
||||
const auto result = ArrangeStackLayout(options, items, UIRect(0.0f, 0.0f, 200.0f, 100.0f));
|
||||
|
||||
ExpectRect(result.children[0].arrangedRect, 8.0f, 6.0f, 184.0f, 10.0f);
|
||||
ExpectRect(result.children[1].arrangedRect, 8.0f, 20.0f, 60.0f, 20.0f);
|
||||
}
|
||||
|
||||
TEST(UI_Layout, ArrangeOverlaySupportsCenterAndStretch) {
|
||||
UIOverlayLayoutOptions options = {};
|
||||
options.padding = UILayoutThickness::Uniform(10.0f);
|
||||
|
||||
std::vector<UILayoutItem> items(2);
|
||||
items[0].desiredContentSize = UISize(40.0f, 20.0f);
|
||||
items[0].horizontalAlignment = UILayoutAlignment::Center;
|
||||
items[0].verticalAlignment = UILayoutAlignment::Center;
|
||||
|
||||
items[1].desiredContentSize = UISize(10.0f, 10.0f);
|
||||
items[1].width = UILayoutLength::Stretch();
|
||||
items[1].height = UILayoutLength::Stretch();
|
||||
items[1].margin = UILayoutThickness::Uniform(5.0f);
|
||||
|
||||
const auto result = ArrangeOverlayLayout(options, items, UIRect(0.0f, 0.0f, 100.0f, 60.0f));
|
||||
|
||||
ExpectRect(result.children[0].arrangedRect, 30.0f, 20.0f, 40.0f, 20.0f);
|
||||
ExpectRect(result.children[1].arrangedRect, 15.0f, 15.0f, 70.0f, 30.0f);
|
||||
}
|
||||
|
||||
TEST(UI_Layout, MeasureOverlayRespectsItemMinMaxAndAvailableConstraints) {
|
||||
UIOverlayLayoutOptions options = {};
|
||||
|
||||
std::vector<UILayoutItem> items(1);
|
||||
items[0].width = UILayoutLength::Pixels(500.0f);
|
||||
items[0].desiredContentSize = UISize(10.0f, 10.0f);
|
||||
items[0].minSize = UISize(0.0f, 50.0f);
|
||||
items[0].maxSize = UISize(200.0f, 120.0f);
|
||||
|
||||
const auto result = MeasureOverlayLayout(
|
||||
options,
|
||||
items,
|
||||
UILayoutConstraints::Bounded(150.0f, 100.0f));
|
||||
|
||||
EXPECT_FLOAT_EQ(result.children[0].measuredSize.width, 150.0f);
|
||||
EXPECT_FLOAT_EQ(result.children[0].measuredSize.height, 50.0f);
|
||||
EXPECT_FLOAT_EQ(result.desiredSize.width, 150.0f);
|
||||
EXPECT_FLOAT_EQ(result.desiredSize.height, 50.0f);
|
||||
}
|
||||
166
tests/Core/UI/test_style_system.cpp
Normal file
166
tests/Core/UI/test_style_system.cpp
Normal file
@@ -0,0 +1,166 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Style/StyleResolver.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Math::Color;
|
||||
using XCEngine::UI::Style::BuildBuiltinTheme;
|
||||
using XCEngine::UI::Style::BuildTheme;
|
||||
using XCEngine::UI::Style::UICornerRadius;
|
||||
using XCEngine::UI::Style::UIBuiltinThemeKind;
|
||||
using XCEngine::UI::Style::UIResolvedStyle;
|
||||
using XCEngine::UI::Style::UIStyleLayer;
|
||||
using XCEngine::UI::Style::UIStylePropertyId;
|
||||
using XCEngine::UI::Style::UIStyleResolveContext;
|
||||
using XCEngine::UI::Style::UIStyleSet;
|
||||
using XCEngine::UI::Style::UIStyleSheet;
|
||||
using XCEngine::UI::Style::UIStyleValue;
|
||||
using XCEngine::UI::Style::UIStyleValueType;
|
||||
using XCEngine::UI::Style::UITheme;
|
||||
using XCEngine::UI::Style::UIThemeDefinition;
|
||||
using XCEngine::UI::Style::UITokenResolveStatus;
|
||||
|
||||
void ExpectColorEq(const Color& actual, const Color& 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(UI_StyleSystem, ThemeDefinitionBuildsThemeAndResolvesAliases) {
|
||||
UIThemeDefinition definition = {};
|
||||
definition.name = "CustomTheme";
|
||||
definition.SetToken("space.base", UIStyleValue(8.0f));
|
||||
definition.SetToken("gap.control", UIStyleValue::Token("space.base"));
|
||||
definition.SetToken("radius.card", UIStyleValue(UICornerRadius::Uniform(12.0f)));
|
||||
|
||||
const UITheme theme = BuildTheme(definition);
|
||||
|
||||
EXPECT_EQ(theme.GetName(), "CustomTheme");
|
||||
|
||||
const auto gapToken = theme.ResolveToken("gap.control", UIStyleValueType::Float);
|
||||
ASSERT_EQ(gapToken.status, UITokenResolveStatus::Resolved);
|
||||
ASSERT_NE(gapToken.value.TryGetFloat(), nullptr);
|
||||
EXPECT_FLOAT_EQ(*gapToken.value.TryGetFloat(), 8.0f);
|
||||
|
||||
const auto radiusToken = theme.ResolveToken("radius.card", UIStyleValueType::CornerRadius);
|
||||
ASSERT_EQ(radiusToken.status, UITokenResolveStatus::Resolved);
|
||||
ASSERT_NE(radiusToken.value.TryGetCornerRadius(), nullptr);
|
||||
EXPECT_TRUE(radiusToken.value.TryGetCornerRadius()->IsUniform());
|
||||
EXPECT_FLOAT_EQ(radiusToken.value.TryGetCornerRadius()->topLeft, 12.0f);
|
||||
}
|
||||
|
||||
TEST(UI_StyleSystem, ThemeResolvesParentTokensAndBuiltinVariantsDiffer) {
|
||||
UIThemeDefinition baseDefinition = {};
|
||||
baseDefinition.name = "Base";
|
||||
baseDefinition.SetToken("color.surface", UIStyleValue(Color(0.20f, 0.21f, 0.22f, 1.0f)));
|
||||
const UITheme baseTheme = BuildTheme(baseDefinition);
|
||||
|
||||
UIThemeDefinition childDefinition = {};
|
||||
childDefinition.name = "Child";
|
||||
childDefinition.SetToken("font.body", UIStyleValue(15.0f));
|
||||
const UITheme childTheme = BuildTheme(childDefinition, &baseTheme);
|
||||
|
||||
const auto parentToken = childTheme.ResolveToken("color.surface", UIStyleValueType::Color);
|
||||
ASSERT_EQ(parentToken.status, UITokenResolveStatus::Resolved);
|
||||
ASSERT_NE(parentToken.value.TryGetColor(), nullptr);
|
||||
ExpectColorEq(*parentToken.value.TryGetColor(), Color(0.20f, 0.21f, 0.22f, 1.0f));
|
||||
|
||||
const UITheme darkTheme = BuildBuiltinTheme(UIBuiltinThemeKind::NeutralDark);
|
||||
const UITheme lightTheme = BuildBuiltinTheme(UIBuiltinThemeKind::NeutralLight);
|
||||
const auto darkSurface = darkTheme.ResolveToken("color.surface", UIStyleValueType::Color);
|
||||
const auto lightSurface = lightTheme.ResolveToken("color.surface", UIStyleValueType::Color);
|
||||
ASSERT_EQ(darkSurface.status, UITokenResolveStatus::Resolved);
|
||||
ASSERT_EQ(lightSurface.status, UITokenResolveStatus::Resolved);
|
||||
ASSERT_NE(darkSurface.value.TryGetColor(), nullptr);
|
||||
ASSERT_NE(lightSurface.value.TryGetColor(), nullptr);
|
||||
EXPECT_NE(darkSurface.value.TryGetColor()->r, lightSurface.value.TryGetColor()->r);
|
||||
}
|
||||
|
||||
TEST(UI_StyleSystem, ThemeReportsMissingCyclesAndTypeMismatches) {
|
||||
UIThemeDefinition definition = {};
|
||||
definition.SetToken("cycle.a", UIStyleValue::Token("cycle.b"));
|
||||
definition.SetToken("cycle.b", UIStyleValue::Token("cycle.a"));
|
||||
definition.SetToken("color.surface", UIStyleValue(Color(0.1f, 0.2f, 0.3f, 1.0f)));
|
||||
const UITheme theme = BuildTheme(definition);
|
||||
|
||||
EXPECT_EQ(
|
||||
theme.ResolveToken("missing.token", UIStyleValueType::Float).status,
|
||||
UITokenResolveStatus::MissingToken);
|
||||
EXPECT_EQ(
|
||||
theme.ResolveToken("cycle.a", UIStyleValueType::Float).status,
|
||||
UITokenResolveStatus::CycleDetected);
|
||||
EXPECT_EQ(
|
||||
theme.ResolveToken("color.surface", UIStyleValueType::Float).status,
|
||||
UITokenResolveStatus::TypeMismatch);
|
||||
}
|
||||
|
||||
TEST(UI_StyleSystem, StyleResolutionPrefersLocalThenNamedThenTypeThenDefault) {
|
||||
UIStyleSheet styleSheet = {};
|
||||
styleSheet.DefaultStyle().SetProperty(UIStylePropertyId::FontSize, UIStyleValue(12.0f));
|
||||
styleSheet.GetOrCreateTypeStyle("Button").SetProperty(UIStylePropertyId::FontSize, UIStyleValue(14.0f));
|
||||
styleSheet.GetOrCreateNamedStyle("Primary").SetProperty(UIStylePropertyId::FontSize, UIStyleValue(16.0f));
|
||||
|
||||
UIStyleSet localStyle = {};
|
||||
localStyle.SetProperty(UIStylePropertyId::FontSize, UIStyleValue(18.0f));
|
||||
|
||||
UIStyleResolveContext context = {};
|
||||
context.styleSheet = &styleSheet;
|
||||
context.selector.typeName = "Button";
|
||||
context.selector.styleName = "Primary";
|
||||
context.localStyle = &localStyle;
|
||||
|
||||
auto resolution = XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::FontSize, context);
|
||||
ASSERT_TRUE(resolution.resolved);
|
||||
EXPECT_EQ(resolution.layer, UIStyleLayer::Local);
|
||||
ASSERT_NE(resolution.value.TryGetFloat(), nullptr);
|
||||
EXPECT_FLOAT_EQ(*resolution.value.TryGetFloat(), 18.0f);
|
||||
|
||||
localStyle.RemoveProperty(UIStylePropertyId::FontSize);
|
||||
resolution = XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::FontSize, context);
|
||||
ASSERT_TRUE(resolution.resolved);
|
||||
EXPECT_EQ(resolution.layer, UIStyleLayer::Named);
|
||||
EXPECT_FLOAT_EQ(*resolution.value.TryGetFloat(), 16.0f);
|
||||
|
||||
styleSheet.GetOrCreateNamedStyle("Primary").RemoveProperty(UIStylePropertyId::FontSize);
|
||||
resolution = XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::FontSize, context);
|
||||
ASSERT_TRUE(resolution.resolved);
|
||||
EXPECT_EQ(resolution.layer, UIStyleLayer::Type);
|
||||
EXPECT_FLOAT_EQ(*resolution.value.TryGetFloat(), 14.0f);
|
||||
}
|
||||
|
||||
TEST(UI_StyleSystem, StyleResolutionFallsBackWhenHigherPriorityTokenCannotResolve) {
|
||||
UIThemeDefinition themeDefinition = {};
|
||||
themeDefinition.SetToken("color.accent", UIStyleValue(Color(0.90f, 0.20f, 0.10f, 1.0f)));
|
||||
const UITheme theme = BuildTheme(themeDefinition);
|
||||
|
||||
UIStyleSheet styleSheet = {};
|
||||
styleSheet.DefaultStyle().SetProperty(UIStylePropertyId::BorderWidth, UIStyleValue(1.0f));
|
||||
styleSheet.GetOrCreateTypeStyle("Button")
|
||||
.SetProperty(UIStylePropertyId::BackgroundColor, UIStyleValue::Token("color.accent"));
|
||||
styleSheet.GetOrCreateNamedStyle("Danger")
|
||||
.SetProperty(UIStylePropertyId::BackgroundColor, UIStyleValue::Token("missing.token"));
|
||||
|
||||
UIStyleResolveContext context = {};
|
||||
context.theme = &theme;
|
||||
context.styleSheet = &styleSheet;
|
||||
context.selector.typeName = "Button";
|
||||
context.selector.styleName = "Danger";
|
||||
|
||||
const auto backgroundResolution =
|
||||
XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::BackgroundColor, context);
|
||||
ASSERT_TRUE(backgroundResolution.resolved);
|
||||
EXPECT_EQ(backgroundResolution.layer, UIStyleLayer::Type);
|
||||
ASSERT_NE(backgroundResolution.value.TryGetColor(), nullptr);
|
||||
ExpectColorEq(*backgroundResolution.value.TryGetColor(), Color(0.90f, 0.20f, 0.10f, 1.0f));
|
||||
|
||||
const UIResolvedStyle resolvedStyle = XCEngine::UI::Style::ResolveStyle(context);
|
||||
const auto* borderWidthResolution = resolvedStyle.FindProperty(UIStylePropertyId::BorderWidth);
|
||||
ASSERT_NE(borderWidthResolution, nullptr);
|
||||
EXPECT_EQ(borderWidthResolution->layer, UIStyleLayer::Default);
|
||||
ASSERT_NE(borderWidthResolution->value.TryGetFloat(), nullptr);
|
||||
EXPECT_FLOAT_EQ(*borderWidthResolution->value.TryGetFloat(), 1.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
326
tests/Core/UI/test_ui_runtime.cpp
Normal file
326
tests/Core/UI/test_ui_runtime.cpp
Normal file
@@ -0,0 +1,326 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||||
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||
#include <XCEngine/UI/Runtime/UISystem.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
using XCEngine::UI::UIColor;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Runtime::IUIScreenDocumentHost;
|
||||
using XCEngine::UI::Runtime::UIScreenAsset;
|
||||
using XCEngine::UI::Runtime::UIScreenDocument;
|
||||
using XCEngine::UI::Runtime::UIScreenFrameInput;
|
||||
using XCEngine::UI::Runtime::UIScreenFrameResult;
|
||||
using XCEngine::UI::Runtime::UIScreenLayerId;
|
||||
using XCEngine::UI::Runtime::UIScreenLayerOptions;
|
||||
using XCEngine::UI::Runtime::UIScreenLoadResult;
|
||||
using XCEngine::UI::Runtime::UIScreenPlayer;
|
||||
using XCEngine::UI::Runtime::UIDocumentScreenHost;
|
||||
using XCEngine::UI::Runtime::UISystemFrameResult;
|
||||
using XCEngine::UI::Runtime::UISystem;
|
||||
|
||||
class FakeScreenDocumentHost final : public IUIScreenDocumentHost {
|
||||
public:
|
||||
struct BuildCall {
|
||||
std::string displayName = {};
|
||||
std::size_t inputEventCount = 0;
|
||||
std::uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
UIScreenLoadResult LoadScreen(const UIScreenAsset& asset) override {
|
||||
++loadCount;
|
||||
lastLoadedAsset = asset;
|
||||
|
||||
UIScreenLoadResult result = {};
|
||||
if (!asset.IsValid()) {
|
||||
result.errorMessage = "Invalid screen asset.";
|
||||
return result;
|
||||
}
|
||||
|
||||
result.succeeded = true;
|
||||
result.document.sourcePath = asset.documentPath;
|
||||
result.document.displayName = asset.screenId.empty() ? asset.documentPath : asset.screenId;
|
||||
result.document.viewDocument.valid = true;
|
||||
result.document.dependencies.push_back(asset.themePath);
|
||||
return result;
|
||||
}
|
||||
|
||||
UIScreenFrameResult BuildFrame(
|
||||
const UIScreenDocument& document,
|
||||
const UIScreenFrameInput& input) override {
|
||||
++buildCount;
|
||||
lastBuiltDocument = document;
|
||||
lastFrameInput = input;
|
||||
|
||||
UIScreenFrameResult result = {};
|
||||
UIDrawList& drawList = result.drawData.EmplaceDrawList(document.displayName);
|
||||
drawList.AddFilledRect(input.viewportRect, UIColor(0.2f, 0.3f, 0.4f, 1.0f), 4.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(input.viewportRect.x + 8.0f, input.viewportRect.y + 8.0f),
|
||||
document.displayName,
|
||||
UIColor(1.0f, 1.0f, 1.0f, 1.0f),
|
||||
16.0f);
|
||||
buildCalls.push_back(BuildCall{
|
||||
document.displayName,
|
||||
input.events.size(),
|
||||
input.frameIndex
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
std::size_t loadCount = 0;
|
||||
std::size_t buildCount = 0;
|
||||
UIScreenAsset lastLoadedAsset = {};
|
||||
UIScreenDocument lastBuiltDocument = {};
|
||||
UIScreenFrameInput lastFrameInput = {};
|
||||
std::vector<BuildCall> buildCalls = {};
|
||||
};
|
||||
|
||||
UIScreenAsset MakeAsset() {
|
||||
UIScreenAsset asset = {};
|
||||
asset.screenId = "MainMenu";
|
||||
asset.documentPath = "Assets/UI/MainMenu.xcui";
|
||||
asset.themePath = "Assets/UI/MainMenu.xctheme";
|
||||
return asset;
|
||||
}
|
||||
|
||||
UIScreenFrameInput MakeFrameInput(std::uint64_t frameIndex = 7) {
|
||||
UIScreenFrameInput input = {};
|
||||
input.viewportRect = UIRect(10.0f, 20.0f, 320.0f, 180.0f);
|
||||
input.deltaTimeSeconds = 1.0 / 60.0;
|
||||
input.frameIndex = frameIndex;
|
||||
input.focused = true;
|
||||
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::PointerMove;
|
||||
event.position = UIPoint(42.0f, 64.0f);
|
||||
input.events.push_back(event);
|
||||
return input;
|
||||
}
|
||||
|
||||
void WriteTextFile(const fs::path& path, const char* contents) {
|
||||
fs::create_directories(path.parent_path());
|
||||
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
||||
ASSERT_TRUE(output.is_open());
|
||||
output << contents;
|
||||
ASSERT_TRUE(static_cast<bool>(output));
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, ScreenPlayerLoadsAssetAndDocumentMetadata) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
|
||||
ASSERT_TRUE(player.Load(MakeAsset()));
|
||||
ASSERT_TRUE(player.IsLoaded());
|
||||
ASSERT_NE(player.GetAsset(), nullptr);
|
||||
ASSERT_NE(player.GetDocument(), nullptr);
|
||||
EXPECT_EQ(player.GetAsset()->documentPath, "Assets/UI/MainMenu.xcui");
|
||||
EXPECT_EQ(player.GetDocument()->displayName, "MainMenu");
|
||||
EXPECT_EQ(host.loadCount, 1u);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, ScreenPlayerUpdateBuildsFrameAndTracksStats) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
ASSERT_TRUE(player.Load(MakeAsset()));
|
||||
|
||||
const UIScreenFrameResult& result = player.Update(MakeFrameInput(12));
|
||||
|
||||
EXPECT_TRUE(result.errorMessage.empty());
|
||||
EXPECT_TRUE(result.stats.documentLoaded);
|
||||
EXPECT_EQ(result.stats.drawListCount, 1u);
|
||||
EXPECT_EQ(result.stats.commandCount, 2u);
|
||||
EXPECT_EQ(result.stats.inputEventCount, 1u);
|
||||
EXPECT_EQ(result.stats.presentedFrameIndex, 12u);
|
||||
EXPECT_EQ(player.GetPresentedFrameCount(), 1u);
|
||||
EXPECT_EQ(host.buildCount, 1u);
|
||||
EXPECT_EQ(host.lastBuiltDocument.displayName, "MainMenu");
|
||||
EXPECT_EQ(host.lastFrameInput.viewportRect.width, 320.0f);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, ScreenPlayerWithoutLoadedDocumentReturnsNotLoadedFrame) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
|
||||
const UIScreenFrameResult& result = player.Update(MakeFrameInput());
|
||||
|
||||
EXPECT_FALSE(result.stats.documentLoaded);
|
||||
EXPECT_TRUE(result.drawData.Empty());
|
||||
EXPECT_FALSE(result.errorMessage.empty());
|
||||
EXPECT_EQ(player.GetPresentedFrameCount(), 0u);
|
||||
EXPECT_EQ(host.buildCount, 0u);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, UISystemTicksAllCreatedPlayers) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
UISystem system(host);
|
||||
UIScreenPlayer& playerA = system.CreatePlayer();
|
||||
UIScreenPlayer& playerB = system.CreatePlayer();
|
||||
|
||||
ASSERT_TRUE(playerA.Load(MakeAsset()));
|
||||
|
||||
UIScreenAsset hudAsset = MakeAsset();
|
||||
hudAsset.screenId = "HUD";
|
||||
hudAsset.documentPath = "Assets/UI/Hud.xcui";
|
||||
ASSERT_TRUE(playerB.Load(hudAsset));
|
||||
|
||||
system.Tick(MakeFrameInput(21));
|
||||
|
||||
EXPECT_EQ(system.GetPlayerCount(), 2u);
|
||||
EXPECT_EQ(host.loadCount, 2u);
|
||||
EXPECT_EQ(host.buildCount, 2u);
|
||||
EXPECT_EQ(playerA.GetLastFrame().stats.presentedFrameIndex, 21u);
|
||||
EXPECT_EQ(playerB.GetLastFrame().stats.presentedFrameIndex, 21u);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, UISystemUpdateComposesLayersAndRoutesInputToTopInteractiveLayer) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
UISystem system(host);
|
||||
|
||||
UIScreenPlayer& gameplay = system.CreatePlayer();
|
||||
ASSERT_TRUE(gameplay.Load(MakeAsset()));
|
||||
|
||||
UIScreenAsset hudAsset = MakeAsset();
|
||||
hudAsset.screenId = "HUD";
|
||||
hudAsset.documentPath = "Assets/UI/Hud.xcui";
|
||||
UIScreenLayerOptions hudOptions = {};
|
||||
hudOptions.debugName = "HUD";
|
||||
hudOptions.acceptsInput = false;
|
||||
UIScreenPlayer& hud = system.CreatePlayer(hudOptions);
|
||||
ASSERT_TRUE(hud.Load(hudAsset));
|
||||
|
||||
const UISystemFrameResult& frame = system.Update(MakeFrameInput(33));
|
||||
|
||||
ASSERT_EQ(frame.presentedLayerCount, 2u);
|
||||
EXPECT_EQ(frame.skippedLayerCount, 0u);
|
||||
ASSERT_EQ(frame.layers.size(), 2u);
|
||||
EXPECT_EQ(frame.layers[0].asset.screenId, "MainMenu");
|
||||
EXPECT_EQ(frame.layers[1].asset.screenId, "HUD");
|
||||
EXPECT_EQ(frame.layers[0].stats.inputEventCount, 1u);
|
||||
EXPECT_EQ(frame.layers[1].stats.inputEventCount, 0u);
|
||||
|
||||
ASSERT_EQ(frame.drawData.GetDrawListCount(), 2u);
|
||||
EXPECT_EQ(frame.drawData.GetDrawLists()[0].GetDebugName(), "MainMenu");
|
||||
EXPECT_EQ(frame.drawData.GetDrawLists()[1].GetDebugName(), "HUD");
|
||||
|
||||
ASSERT_EQ(host.buildCalls.size(), 2u);
|
||||
EXPECT_EQ(host.buildCalls[0].displayName, "MainMenu");
|
||||
EXPECT_EQ(host.buildCalls[0].inputEventCount, 1u);
|
||||
EXPECT_EQ(host.buildCalls[1].displayName, "HUD");
|
||||
EXPECT_EQ(host.buildCalls[1].inputEventCount, 0u);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, UISystemModalLayerBlocksLowerLayersAndKeepsOnlyTopFrameVisible) {
|
||||
FakeScreenDocumentHost host = {};
|
||||
UISystem system(host);
|
||||
|
||||
UIScreenAsset gameplayAsset = MakeAsset();
|
||||
gameplayAsset.screenId = "GameplayHUD";
|
||||
gameplayAsset.documentPath = "Assets/UI/GameplayHud.xcui";
|
||||
const UIScreenLayerId gameplayLayer = system.PushScreen(gameplayAsset);
|
||||
ASSERT_NE(gameplayLayer, 0u);
|
||||
|
||||
UIScreenAsset pauseAsset = MakeAsset();
|
||||
pauseAsset.screenId = "PauseMenu";
|
||||
pauseAsset.documentPath = "Assets/UI/PauseMenu.xcui";
|
||||
UIScreenLayerOptions pauseOptions = {};
|
||||
pauseOptions.debugName = "PauseMenu";
|
||||
pauseOptions.blocksLayersBelow = true;
|
||||
const UIScreenLayerId pauseLayer = system.PushScreen(pauseAsset, pauseOptions);
|
||||
ASSERT_NE(pauseLayer, 0u);
|
||||
|
||||
const UISystemFrameResult& frame = system.Update(MakeFrameInput(48));
|
||||
|
||||
EXPECT_EQ(system.GetLayerCount(), 2u);
|
||||
EXPECT_EQ(frame.presentedLayerCount, 1u);
|
||||
EXPECT_EQ(frame.skippedLayerCount, 1u);
|
||||
ASSERT_EQ(frame.layers.size(), 1u);
|
||||
EXPECT_EQ(frame.layers[0].layerId, pauseLayer);
|
||||
EXPECT_EQ(frame.layers[0].asset.screenId, "PauseMenu");
|
||||
EXPECT_TRUE(frame.layers[0].options.blocksLayersBelow);
|
||||
ASSERT_EQ(frame.drawData.GetDrawListCount(), 1u);
|
||||
EXPECT_EQ(frame.drawData.GetDrawLists()[0].GetDebugName(), "PauseMenu");
|
||||
|
||||
ASSERT_EQ(host.buildCalls.size(), 1u);
|
||||
EXPECT_EQ(host.buildCalls[0].displayName, "PauseMenu");
|
||||
EXPECT_EQ(host.buildCalls[0].inputEventCount, 1u);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, UIDocumentScreenHostLoadsRealCompiledDocuments) {
|
||||
const fs::path root = fs::temp_directory_path() / "xcui_runtime_host_load_test";
|
||||
fs::remove_all(root);
|
||||
|
||||
WriteTextFile(
|
||||
root / "RuntimeScreen.xcui",
|
||||
"<View name=\"RuntimeScreen\">\n"
|
||||
" <Column gap=\"10\">\n"
|
||||
" <Text text=\"Runtime HUD\" />\n"
|
||||
" </Column>\n"
|
||||
"</View>\n");
|
||||
WriteTextFile(root / "RuntimeTheme.xctheme", "<Theme name=\"RuntimeTheme\" />\n");
|
||||
|
||||
UIDocumentScreenHost host = {};
|
||||
UIScreenAsset asset = {};
|
||||
asset.screenId = "RuntimeHUD";
|
||||
asset.documentPath = (root / "RuntimeScreen.xcui").string();
|
||||
asset.themePath = (root / "RuntimeTheme.xctheme").string();
|
||||
|
||||
const UIScreenLoadResult loadResult = host.LoadScreen(asset);
|
||||
ASSERT_TRUE(loadResult.succeeded);
|
||||
EXPECT_EQ(loadResult.document.displayName, "RuntimeHUD");
|
||||
EXPECT_TRUE(loadResult.document.viewDocument.valid);
|
||||
EXPECT_TRUE(loadResult.document.hasThemeDocument);
|
||||
EXPECT_FALSE(loadResult.document.dependencies.empty());
|
||||
|
||||
fs::remove_all(root);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, UIDocumentScreenHostBuildsConcreteRuntimeFrame) {
|
||||
const fs::path root = fs::temp_directory_path() / "xcui_runtime_host_frame_test";
|
||||
fs::remove_all(root);
|
||||
|
||||
WriteTextFile(
|
||||
root / "RuntimeScreen.xcui",
|
||||
"<View name=\"RuntimeScreen\" padding=\"18\">\n"
|
||||
" <Column gap=\"10\">\n"
|
||||
" <Text text=\"Runtime HUD\" />\n"
|
||||
" <Card title=\"Quest Tracker\" subtitle=\"2 active objectives\">\n"
|
||||
" <Text text=\"Collect 3 relic shards\" />\n"
|
||||
" </Card>\n"
|
||||
" <Row gap=\"12\">\n"
|
||||
" <Button title=\"Resume\" width=\"stretch\" />\n"
|
||||
" <Button title=\"Settings\" width=\"stretch\" />\n"
|
||||
" </Row>\n"
|
||||
" </Column>\n"
|
||||
"</View>\n");
|
||||
|
||||
UIDocumentScreenHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
UIScreenAsset asset = {};
|
||||
asset.documentPath = (root / "RuntimeScreen.xcui").string();
|
||||
ASSERT_TRUE(player.Load(asset));
|
||||
|
||||
const UIScreenFrameResult& frame = player.Update(MakeFrameInput(33));
|
||||
EXPECT_TRUE(frame.errorMessage.empty());
|
||||
EXPECT_TRUE(frame.stats.documentLoaded);
|
||||
EXPECT_GT(frame.stats.commandCount, 0u);
|
||||
EXPECT_GT(frame.stats.nodeCount, 0u);
|
||||
EXPECT_GT(frame.stats.filledRectCommandCount, 0u);
|
||||
EXPECT_GT(frame.stats.textCommandCount, 0u);
|
||||
EXPECT_EQ(frame.stats.presentedFrameIndex, 33u);
|
||||
|
||||
fs::remove_all(root);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user