Add XCUI layout engine MVP and archive subplan 02

This commit is contained in:
2026-04-04 19:13:16 +08:00
parent c2cb2e5914
commit 341ce79231
5 changed files with 811 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
# XCUI Subplan 02Layout Engine 完成归档
归档日期:
- `2026-04-04`
原始来源:
- [../XCUI完整架构设计与执行计划.md](../XCUI完整架构设计与执行计划.md)
本次完成范围:
- 落地 XCUI 纯算法布局基础类型:
- `UILayoutLength`
- `UILayoutConstraints`
- `UILayoutThickness`
- `UILayoutItem`
- `UIStackLayoutOptions`
- `UIOverlayLayoutOptions`
- 落地 measure / arrange 双阶段布局算法
- 实现 `Horizontal Stack` / `Vertical Stack` / `Overlay` 三类 MVP 容器
- 支持 `px / content / stretch`
- 支持 `padding / spacing / margin / min / max / alignment`
- 建立独立 `ui_tests` 测试目标并通过验证
实际代码落点:
- [engine/include/XCEngine/UI/Types.h](D:/Xuanchi/Main/XCEngine/engine/include/XCEngine/UI/Types.h)
- [engine/include/XCEngine/UI/Layout/LayoutTypes.h](D:/Xuanchi/Main/XCEngine/engine/include/XCEngine/UI/Layout/LayoutTypes.h)
- [engine/include/XCEngine/UI/Layout/LayoutEngine.h](D:/Xuanchi/Main/XCEngine/engine/include/XCEngine/UI/Layout/LayoutEngine.h)
- [tests/Core/Math/CMakeLists.txt](D:/Xuanchi/Main/XCEngine/tests/Core/Math/CMakeLists.txt)
- [tests/Core/Math/test_ui_layout.cpp](D:/Xuanchi/Main/XCEngine/tests/Core/Math/test_ui_layout.cpp)
验证结果:
- `cmake --build . --config Debug --target math_tests -- /m:1`
- `ctest -C Debug --test-dir D:\\Xuanchi\\Main\\XCEngine\\build\\tests\\Core\\Math --output-on-failure -R UI_Layout`
- 结果:`5/5 UI_Layout tests passed`
与原子计划相比,当前仍未覆盖:
- `Scroll` 容器
- 更复杂的主轴分布策略
- 与 XCUI tree/state 的正式对接
- 文本测量与真实控件树集成
建议后续承接:
-`Subplan-01` 提供 tree / node / invalidation 契约后,把当前布局算法接入正式 UI tree
- 后续再补 `Scroll`、更完整容器族、文本测量桥接

View File

@@ -0,0 +1,485 @@
#pragma once
#include <XCEngine/UI/Layout/LayoutTypes.h>
#include <algorithm>
#include <cmath>
#include <vector>
namespace XCEngine {
namespace UI {
namespace Layout {
struct UILayoutPassResult {
UISize desiredSize = UISize(0.0f, 0.0f);
std::vector<UILayoutChildResult> children = {};
};
namespace Detail {
inline bool IsFiniteExtent(float value) {
return std::isfinite(value);
}
inline float ClampExtent(float value, float minValue, float maxValue) {
value = (std::max)(value, minValue);
if (IsFiniteExtent(maxValue)) {
value = (std::min)(value, maxValue);
}
return value;
}
inline float ReduceAvailableExtent(float value, float reduction) {
if (!IsFiniteExtent(value)) {
return value;
}
return (std::max)(0.0f, value - reduction);
}
inline UISize DeflateSize(const UISize& size, const UILayoutThickness& thickness) {
return UISize(
ReduceAvailableExtent(size.width, thickness.Horizontal()),
ReduceAvailableExtent(size.height, thickness.Vertical()));
}
inline UIRect DeflateRect(const UIRect& rect, const UILayoutThickness& thickness) {
const float width = (std::max)(0.0f, rect.width - thickness.Horizontal());
const float height = (std::max)(0.0f, rect.height - thickness.Vertical());
return UIRect(rect.x + thickness.left, rect.y + thickness.top, width, height);
}
inline float ResolveMeasuredExtent(
const UILayoutLength& length,
float desiredContentExtent,
float minExtent,
float maxExtent,
float availableExtent) {
float resolved = desiredContentExtent;
switch (length.unit) {
case UILayoutLengthUnit::Pixels:
resolved = length.value;
break;
case UILayoutLengthUnit::Stretch:
case UILayoutLengthUnit::Auto:
default:
resolved = desiredContentExtent;
break;
}
float effectiveMaxExtent = maxExtent;
if (IsFiniteExtent(availableExtent)) {
effectiveMaxExtent = IsFiniteExtent(effectiveMaxExtent)
? (std::min)(effectiveMaxExtent, availableExtent)
: availableExtent;
}
return ClampExtent(resolved, minExtent, effectiveMaxExtent);
}
inline UISize MeasureItemBaseSize(const UILayoutItem& item, const UILayoutConstraints& constraints) {
UISize measuredSize = {};
measuredSize.width = ResolveMeasuredExtent(
item.width,
item.desiredContentSize.width,
item.minSize.width,
item.maxSize.width,
constraints.maxSize.width);
measuredSize.height = ResolveMeasuredExtent(
item.height,
item.desiredContentSize.height,
item.minSize.height,
item.maxSize.height,
constraints.maxSize.height);
return measuredSize;
}
inline float GetMainExtent(const UISize& size, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? size.width : size.height;
}
inline float GetCrossExtent(const UISize& size, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? size.height : size.width;
}
inline float GetLeadingMargin(const UILayoutThickness& thickness, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? thickness.left : thickness.top;
}
inline float GetTrailingMargin(const UILayoutThickness& thickness, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? thickness.right : thickness.bottom;
}
inline float GetMainMargin(const UILayoutThickness& thickness, UILayoutAxis axis) {
return GetLeadingMargin(thickness, axis) + GetTrailingMargin(thickness, axis);
}
inline float GetCrossMargin(const UILayoutThickness& thickness, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal
? thickness.top + thickness.bottom
: thickness.left + thickness.right;
}
inline UILayoutLength GetMainLength(const UILayoutItem& item, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? item.width : item.height;
}
inline UILayoutLength GetCrossLength(const UILayoutItem& item, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? item.height : item.width;
}
inline float GetMinMainExtent(const UILayoutItem& item, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? item.minSize.width : item.minSize.height;
}
inline float GetMinCrossExtent(const UILayoutItem& item, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? item.minSize.height : item.minSize.width;
}
inline float GetMaxMainExtent(const UILayoutItem& item, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? item.maxSize.width : item.maxSize.height;
}
inline float GetMaxCrossExtent(const UILayoutItem& item, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? item.maxSize.height : item.maxSize.width;
}
inline UILayoutAlignment GetCrossAlignment(const UILayoutItem& item, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? item.verticalAlignment : item.horizontalAlignment;
}
inline UILayoutAlignment GetHorizontalAlignment(const UILayoutItem& item) {
return item.horizontalAlignment;
}
inline UILayoutAlignment GetVerticalAlignment(const UILayoutItem& item) {
return item.verticalAlignment;
}
inline float ComputeAlignmentOffset(
UILayoutAlignment alignment,
float availableExtent,
float childExtent) {
const float remainingExtent = (std::max)(0.0f, availableExtent - childExtent);
switch (alignment) {
case UILayoutAlignment::Center:
return remainingExtent * 0.5f;
case UILayoutAlignment::End:
return remainingExtent;
case UILayoutAlignment::Start:
case UILayoutAlignment::Stretch:
default:
return 0.0f;
}
}
inline float ResolveArrangedCrossExtent(
const UILayoutItem& item,
UILayoutAxis axis,
float measuredCrossExtent,
float availableCrossExtent) {
const UILayoutLength crossLength = GetCrossLength(item, axis);
const UILayoutAlignment crossAlignment = GetCrossAlignment(item, axis);
float resolvedCrossExtent = measuredCrossExtent;
if ((crossLength.IsStretch() || crossAlignment == UILayoutAlignment::Stretch) &&
IsFiniteExtent(availableCrossExtent)) {
resolvedCrossExtent = availableCrossExtent;
}
float effectiveMaxExtent = GetMaxCrossExtent(item, axis);
if (IsFiniteExtent(availableCrossExtent)) {
effectiveMaxExtent = IsFiniteExtent(effectiveMaxExtent)
? (std::min)(effectiveMaxExtent, availableCrossExtent)
: availableCrossExtent;
}
return ClampExtent(resolvedCrossExtent, GetMinCrossExtent(item, axis), effectiveMaxExtent);
}
inline float ResolveArrangedMainExtent(
const UILayoutItem& item,
UILayoutAxis axis,
float measuredMainExtent,
float extraMainExtent,
float availableMainExtent) {
float resolvedMainExtent = measuredMainExtent;
if (GetMainLength(item, axis).IsStretch()) {
resolvedMainExtent += extraMainExtent;
}
float effectiveMaxExtent = GetMaxMainExtent(item, axis);
if (IsFiniteExtent(availableMainExtent)) {
effectiveMaxExtent = IsFiniteExtent(effectiveMaxExtent)
? (std::min)(effectiveMaxExtent, availableMainExtent)
: availableMainExtent;
}
return ClampExtent(resolvedMainExtent, GetMinMainExtent(item, axis), effectiveMaxExtent);
}
} // namespace Detail
inline UILayoutPassResult MeasureStackLayout(
const UIStackLayoutOptions& options,
const std::vector<UILayoutItem>& items,
const UILayoutConstraints& constraints = UILayoutConstraints::Unbounded()) {
UILayoutPassResult result = {};
result.children.resize(items.size());
const UISize innerMaxSize = Detail::DeflateSize(constraints.maxSize, options.padding);
float accumulatedMainExtent = 0.0f;
float maxCrossExtent = 0.0f;
std::size_t visibleCount = 0;
for (std::size_t index = 0; index < items.size(); ++index) {
const UILayoutItem& item = items[index];
UILayoutChildResult& childResult = result.children[index];
childResult.visible = item.visible;
if (!item.visible) {
continue;
}
UILayoutConstraints childConstraints = {};
childConstraints.maxSize = UISize(
Detail::ReduceAvailableExtent(innerMaxSize.width, item.margin.Horizontal()),
Detail::ReduceAvailableExtent(innerMaxSize.height, item.margin.Vertical()));
childResult.measuredSize = Detail::MeasureItemBaseSize(item, childConstraints);
accumulatedMainExtent +=
Detail::GetMainExtent(childResult.measuredSize, options.axis) +
Detail::GetMainMargin(item.margin, options.axis);
maxCrossExtent = (std::max)(
maxCrossExtent,
Detail::GetCrossExtent(childResult.measuredSize, options.axis) +
Detail::GetCrossMargin(item.margin, options.axis));
++visibleCount;
}
if (visibleCount > 1u) {
accumulatedMainExtent += options.spacing * static_cast<float>(visibleCount - 1u);
}
if (options.axis == UILayoutAxis::Horizontal) {
result.desiredSize.width = accumulatedMainExtent + options.padding.Horizontal();
result.desiredSize.height = maxCrossExtent + options.padding.Vertical();
} else {
result.desiredSize.width = maxCrossExtent + options.padding.Horizontal();
result.desiredSize.height = accumulatedMainExtent + options.padding.Vertical();
}
result.desiredSize.width = Detail::ClampExtent(
result.desiredSize.width,
constraints.minSize.width,
constraints.maxSize.width);
result.desiredSize.height = Detail::ClampExtent(
result.desiredSize.height,
constraints.minSize.height,
constraints.maxSize.height);
return result;
}
inline UILayoutPassResult ArrangeStackLayout(
const UIStackLayoutOptions& options,
const std::vector<UILayoutItem>& items,
const UIRect& bounds) {
UILayoutPassResult result =
MeasureStackLayout(options, items, UILayoutConstraints::Bounded(bounds.width, bounds.height));
const UIRect innerBounds = Detail::DeflateRect(bounds, options.padding);
const float innerMainExtent = options.axis == UILayoutAxis::Horizontal ? innerBounds.width : innerBounds.height;
const float innerCrossExtent = options.axis == UILayoutAxis::Horizontal ? innerBounds.height : innerBounds.width;
float totalBaseMainExtent = 0.0f;
float totalStretchWeight = 0.0f;
std::size_t visibleCount = 0u;
for (std::size_t index = 0; index < items.size(); ++index) {
const UILayoutItem& item = items[index];
const UILayoutChildResult& childResult = result.children[index];
if (!item.visible || !childResult.visible) {
continue;
}
totalBaseMainExtent +=
Detail::GetMainExtent(childResult.measuredSize, options.axis) +
Detail::GetMainMargin(item.margin, options.axis);
if (Detail::GetMainLength(item, options.axis).IsStretch()) {
totalStretchWeight += Detail::GetMainLength(item, options.axis).value;
}
++visibleCount;
}
if (visibleCount > 1u) {
totalBaseMainExtent += options.spacing * static_cast<float>(visibleCount - 1u);
}
const float remainingMainExtent = (std::max)(0.0f, innerMainExtent - totalBaseMainExtent);
float mainCursor = options.axis == UILayoutAxis::Horizontal ? innerBounds.x : innerBounds.y;
for (std::size_t index = 0; index < items.size(); ++index) {
const UILayoutItem& item = items[index];
UILayoutChildResult& childResult = result.children[index];
if (!item.visible || !childResult.visible) {
childResult.arrangedRect = UIRect(0.0f, 0.0f, 0.0f, 0.0f);
continue;
}
const float stretchWeight = Detail::GetMainLength(item, options.axis).IsStretch()
? Detail::GetMainLength(item, options.axis).value
: 0.0f;
const float allocatedExtraMainExtent =
totalStretchWeight > 0.0f
? remainingMainExtent * (stretchWeight / totalStretchWeight)
: 0.0f;
const float availableMainExtent = Detail::ReduceAvailableExtent(
innerMainExtent,
Detail::GetMainMargin(item.margin, options.axis));
const float resolvedMainExtent = Detail::ResolveArrangedMainExtent(
item,
options.axis,
Detail::GetMainExtent(childResult.measuredSize, options.axis),
allocatedExtraMainExtent,
availableMainExtent);
const float availableCrossExtent = Detail::ReduceAvailableExtent(
innerCrossExtent,
Detail::GetCrossMargin(item.margin, options.axis));
const float resolvedCrossExtent = Detail::ResolveArrangedCrossExtent(
item,
options.axis,
Detail::GetCrossExtent(childResult.measuredSize, options.axis),
availableCrossExtent);
const float crossOrigin = options.axis == UILayoutAxis::Horizontal
? innerBounds.y + item.margin.top
: innerBounds.x + item.margin.left;
const float crossOffset = Detail::ComputeAlignmentOffset(
Detail::GetCrossAlignment(item, options.axis),
availableCrossExtent,
resolvedCrossExtent);
const float mainOrigin = mainCursor + Detail::GetLeadingMargin(item.margin, options.axis);
if (options.axis == UILayoutAxis::Horizontal) {
childResult.arrangedRect = UIRect(
mainOrigin,
crossOrigin + crossOffset,
resolvedMainExtent,
resolvedCrossExtent);
} else {
childResult.arrangedRect = UIRect(
crossOrigin + crossOffset,
mainOrigin,
resolvedCrossExtent,
resolvedMainExtent);
}
mainCursor +=
Detail::GetLeadingMargin(item.margin, options.axis) +
resolvedMainExtent +
Detail::GetTrailingMargin(item.margin, options.axis) +
options.spacing;
}
return result;
}
inline UILayoutPassResult MeasureOverlayLayout(
const UIOverlayLayoutOptions& options,
const std::vector<UILayoutItem>& items,
const UILayoutConstraints& constraints = UILayoutConstraints::Unbounded()) {
UILayoutPassResult result = {};
result.children.resize(items.size());
const UISize innerMaxSize = Detail::DeflateSize(constraints.maxSize, options.padding);
float desiredWidth = 0.0f;
float desiredHeight = 0.0f;
for (std::size_t index = 0; index < items.size(); ++index) {
const UILayoutItem& item = items[index];
UILayoutChildResult& childResult = result.children[index];
childResult.visible = item.visible;
if (!item.visible) {
continue;
}
UILayoutConstraints childConstraints = {};
childConstraints.maxSize = UISize(
Detail::ReduceAvailableExtent(innerMaxSize.width, item.margin.Horizontal()),
Detail::ReduceAvailableExtent(innerMaxSize.height, item.margin.Vertical()));
childResult.measuredSize = Detail::MeasureItemBaseSize(item, childConstraints);
desiredWidth = (std::max)(desiredWidth, childResult.measuredSize.width + item.margin.Horizontal());
desiredHeight = (std::max)(desiredHeight, childResult.measuredSize.height + item.margin.Vertical());
}
result.desiredSize.width = desiredWidth + options.padding.Horizontal();
result.desiredSize.height = desiredHeight + options.padding.Vertical();
result.desiredSize.width = Detail::ClampExtent(
result.desiredSize.width,
constraints.minSize.width,
constraints.maxSize.width);
result.desiredSize.height = Detail::ClampExtent(
result.desiredSize.height,
constraints.minSize.height,
constraints.maxSize.height);
return result;
}
inline UILayoutPassResult ArrangeOverlayLayout(
const UIOverlayLayoutOptions& options,
const std::vector<UILayoutItem>& items,
const UIRect& bounds) {
UILayoutPassResult result =
MeasureOverlayLayout(options, items, UILayoutConstraints::Bounded(bounds.width, bounds.height));
const UIRect innerBounds = Detail::DeflateRect(bounds, options.padding);
for (std::size_t index = 0; index < items.size(); ++index) {
const UILayoutItem& item = items[index];
UILayoutChildResult& childResult = result.children[index];
if (!item.visible || !childResult.visible) {
childResult.arrangedRect = UIRect(0.0f, 0.0f, 0.0f, 0.0f);
continue;
}
const float availableWidth = Detail::ReduceAvailableExtent(innerBounds.width, item.margin.Horizontal());
const float availableHeight = Detail::ReduceAvailableExtent(innerBounds.height, item.margin.Vertical());
float resolvedWidth = childResult.measuredSize.width;
if ((item.width.IsStretch() || item.horizontalAlignment == UILayoutAlignment::Stretch) &&
Detail::IsFiniteExtent(availableWidth)) {
resolvedWidth = availableWidth;
}
resolvedWidth = Detail::ClampExtent(resolvedWidth, item.minSize.width, item.maxSize.width);
if (Detail::IsFiniteExtent(availableWidth)) {
resolvedWidth = (std::min)(resolvedWidth, availableWidth);
}
float resolvedHeight = childResult.measuredSize.height;
if ((item.height.IsStretch() || item.verticalAlignment == UILayoutAlignment::Stretch) &&
Detail::IsFiniteExtent(availableHeight)) {
resolvedHeight = availableHeight;
}
resolvedHeight = Detail::ClampExtent(resolvedHeight, item.minSize.height, item.maxSize.height);
if (Detail::IsFiniteExtent(availableHeight)) {
resolvedHeight = (std::min)(resolvedHeight, availableHeight);
}
const float originX =
innerBounds.x +
item.margin.left +
Detail::ComputeAlignmentOffset(item.horizontalAlignment, availableWidth, resolvedWidth);
const float originY =
innerBounds.y +
item.margin.top +
Detail::ComputeAlignmentOffset(item.verticalAlignment, availableHeight, resolvedHeight);
childResult.arrangedRect = UIRect(originX, originY, resolvedWidth, resolvedHeight);
}
return result;
}
} // namespace Layout
} // namespace UI
} // namespace XCEngine

View File

@@ -0,0 +1,145 @@
#pragma once
#include <XCEngine/UI/Types.h>
#include <algorithm>
#include <cstdint>
#include <limits>
namespace XCEngine {
namespace UI {
namespace Layout {
inline float GetUnboundedLayoutExtent() {
return std::numeric_limits<float>::infinity();
}
enum class UILayoutAxis : std::uint8_t {
Horizontal = 0,
Vertical
};
enum class UILayoutLengthUnit : std::uint8_t {
Auto = 0,
Pixels,
Stretch
};
enum class UILayoutAlignment : std::uint8_t {
Start = 0,
Center,
End,
Stretch
};
struct UILayoutLength {
UILayoutLengthUnit unit = UILayoutLengthUnit::Auto;
float value = 0.0f;
static UILayoutLength Auto() {
return {};
}
static UILayoutLength Pixels(float pixels) {
UILayoutLength length = {};
length.unit = UILayoutLengthUnit::Pixels;
length.value = (std::max)(0.0f, pixels);
return length;
}
static UILayoutLength Stretch(float weight = 1.0f) {
UILayoutLength length = {};
length.unit = UILayoutLengthUnit::Stretch;
length.value = (std::max)(0.0f, weight);
return length;
}
bool IsStretch() const {
return unit == UILayoutLengthUnit::Stretch && value > 0.0f;
}
};
struct UILayoutThickness {
float left = 0.0f;
float top = 0.0f;
float right = 0.0f;
float bottom = 0.0f;
UILayoutThickness() = default;
UILayoutThickness(float leftValue, float topValue, float rightValue, float bottomValue)
: left((std::max)(0.0f, leftValue))
, top((std::max)(0.0f, topValue))
, right((std::max)(0.0f, rightValue))
, bottom((std::max)(0.0f, bottomValue)) {
}
static UILayoutThickness Uniform(float value) {
return UILayoutThickness(value, value, value, value);
}
static UILayoutThickness Symmetric(float horizontal, float vertical) {
return UILayoutThickness(horizontal, vertical, horizontal, vertical);
}
float Horizontal() const {
return left + right;
}
float Vertical() const {
return top + bottom;
}
};
struct UILayoutConstraints {
UISize minSize = UISize(0.0f, 0.0f);
UISize maxSize = UISize(GetUnboundedLayoutExtent(), GetUnboundedLayoutExtent());
static UILayoutConstraints Unbounded() {
return {};
}
static UILayoutConstraints Bounded(float width, float height) {
UILayoutConstraints constraints = {};
constraints.maxSize = UISize((std::max)(0.0f, width), (std::max)(0.0f, height));
return constraints;
}
static UILayoutConstraints Tight(float width, float height) {
UILayoutConstraints constraints = Bounded(width, height);
constraints.minSize = constraints.maxSize;
return constraints;
}
};
struct UILayoutItem {
UISize desiredContentSize = UISize(0.0f, 0.0f);
UILayoutLength width = UILayoutLength::Auto();
UILayoutLength height = UILayoutLength::Auto();
UISize minSize = UISize(0.0f, 0.0f);
UISize maxSize = UISize(GetUnboundedLayoutExtent(), GetUnboundedLayoutExtent());
UILayoutThickness margin = {};
UILayoutAlignment horizontalAlignment = UILayoutAlignment::Start;
UILayoutAlignment verticalAlignment = UILayoutAlignment::Start;
bool visible = true;
};
struct UIStackLayoutOptions {
UILayoutAxis axis = UILayoutAxis::Vertical;
float spacing = 0.0f;
UILayoutThickness padding = {};
};
struct UIOverlayLayoutOptions {
UILayoutThickness padding = {};
};
struct UILayoutChildResult {
UISize measuredSize = UISize(0.0f, 0.0f);
UIRect arrangedRect = UIRect(0.0f, 0.0f, 0.0f, 0.0f);
bool visible = false;
};
} // namespace Layout
} // namespace UI
} // namespace XCEngine

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

View File

@@ -7,6 +7,7 @@ set(MATH_TEST_SOURCES
test_matrix.cpp
test_quaternion.cpp
test_geometry.cpp
test_ui_layout.cpp
)
add_executable(math_tests ${MATH_TEST_SOURCES})