ADD: LayoutEngine

This commit is contained in:
sebastian 2026-04-17 10:42:10 +02:00
parent 309398d76c
commit 4137db0c1f
4 changed files with 189 additions and 134 deletions

View File

@ -262,6 +262,7 @@ add_executable(Dicewars_Siedler src/main.cpp
src/game/hexWorld/ecs/components/BuildingPreviewComponent.h
src/game/GameWorldSystems.cpp
src/game/GameWorldSystems.h
src/engine/core/gui/uiComponent/layout/LayoutEngine.h
)
target_compile_options(Dicewars_Siedler PRIVATE

View File

@ -0,0 +1,160 @@
//
// Created by sebastian on 17.04.26.
//
#ifndef DICEWARS_SIEDLER_LAYOUTENGINE_H
#define DICEWARS_SIEDLER_LAYOUTENGINE_H
#include <vector>
#include "LayoutStyle.h"
#include "../Dimensions.h"
#endif //DICEWARS_SIEDLER_LAYOUTENGINE_H
namespace LayoutEngine {
struct NodeLayout {
LayoutStyle layoutStyle;
std::vector<NodeLayout> children;
};
struct ComputedLayout {
Dimensions self;
std::vector<ComputedLayout> children;
};
inline float resolveDim(const SizeValue size, float parentSize, float axisLength) {
switch (size.unit) {
case SizeUnit::Percent:
return size.value * parentSize;
case SizeUnit::Pixels:
return size.value / axisLength; // has to be normalized!
default:
return 0.0f;
}
}
inline Dimensions computeSelfDimensions(const NodeLayout &node, const Dimensions &parent, float screenWidth, float screenHeight) {
Dimensions d = {};
d.x = parent.x + resolveDim(node.layoutStyle.margin.left, parent.width, screenWidth);
d.y = parent.y + resolveDim(node.layoutStyle.margin.top, parent.height, screenHeight);
d.width = resolveDim(node.layoutStyle.width, parent.width, screenWidth);
d.height = resolveDim(node.layoutStyle.height, parent.height, screenHeight);
return d;
}
struct JustifyResult {
float offset;
float gap;
};
inline JustifyResult computeJustify(JustifyContent justify, float remainingSpace, std::size_t childCount) {
switch (justify) {
case JustifyContent::Start: return {0.0f, 0.0f};
case JustifyContent::Center: return {remainingSpace / 2.0f, 0.0f};
case JustifyContent::End: return {remainingSpace, 0.0f};
case JustifyContent::SpaceBetween:
if (childCount > 1)
return {0.0f, remainingSpace / (static_cast<float>(childCount) - 1.0f)};
return {0.0f, 0.0f};
default: return {0.0f, 0.0f};
}
}
inline void offsetLayout(ComputedLayout& layout, float dx, float dy) {
layout.self.x += dx;
layout.self.y += dy;
for (auto& child : layout.children) {
offsetLayout(child, dx, dy);
}
}
inline void offsetChildLayouts(ComputedLayout& layout, float dx, float dy) {
for (auto& child : layout.children) {
offsetLayout(child, dx, dy);
}
}
inline ComputedLayout compute(const NodeLayout &node, const Dimensions &parent, float screenWidth, float screenHeight) {
const bool isColumn = (node.layoutStyle.flexDirection == FlexDirection::Column);
ComputedLayout result;
result.self = computeSelfDimensions(node, parent, screenWidth, screenHeight);
result.children.resize(node.children.size());
for (size_t i = 0; i < node.children.size(); i++) {
result.children[i] = compute(node.children[i], result.self, screenWidth, screenHeight);
}
float totalMainSize = 0.0f;
for (size_t i = 0; i < node.children.size(); i++) {
const auto& child = node.children[i];
const auto& childLayout = result.children[i];
const float mainSize = isColumn ? childLayout.self.height : childLayout.self.width;
const float marginMain = isColumn
? resolveDim(child.layoutStyle.margin.top, parent.height, screenHeight)
: resolveDim(child.layoutStyle.margin.left, parent.width, screenWidth);
totalMainSize += mainSize + marginMain;
}
float mainExtent = isColumn ? result.self.height : result.self.width;
float remainingMain = mainExtent - totalMainSize;
auto [justifyOffset, gap] = computeJustify(
node.layoutStyle.justifyContent, remainingMain, node.children.size());
float cursor = justifyOffset;
for (size_t i = 0; i < node.children.size(); i++) {
const auto& child = node.children[i];
auto& childLayout = result.children[i];
if (i > 0 && node.layoutStyle.justifyContent == JustifyContent::SpaceBetween)
cursor += gap;
if (isColumn) {
float marginTop = resolveDim(child.layoutStyle.margin.top, result.self.height, screenHeight);
offsetLayout(childLayout, 0.0f, cursor);
cursor += childLayout.self.height + marginTop;
} else {
float marginLeft = resolveDim(child.layoutStyle.margin.left, result.self.width, screenWidth);
offsetLayout(childLayout, cursor, 0.0f);
cursor += childLayout.self.width + marginLeft;
}
}
for (size_t i = 0; i < node.children.size(); i++) {
const auto& child = node.children[i];
auto& childLayout = result.children[i];
if (isColumn) {
float marginLeft = resolveDim(child.layoutStyle.margin.left, result.self.width, screenWidth);
float remainingCross = result.self.width - childLayout.self.width;
float oldX = childLayout.self.x;
switch (node.layoutStyle.alignItems) {
case AlignItems::Start:
childLayout.self.x = result.self.x + marginLeft; break;
case AlignItems::Center:
childLayout.self.x = result.self.x + remainingCross / 2.0f + marginLeft; break;
case AlignItems::End:
childLayout.self.x = result.self.x + remainingCross + marginLeft; break;
default: break;
}
offsetChildLayouts(childLayout, childLayout.self.x - oldX, 0.0f);
} else {
float marginTop = resolveDim(child.layoutStyle.margin.top, result.self.height, screenHeight);
float remainingCross = result.self.height - childLayout.self.height;
float oldY = childLayout.self.y;
switch (node.layoutStyle.alignItems) {
case AlignItems::Start:
childLayout.self.y = result.self.y + marginTop; break;
case AlignItems::Center:
childLayout.self.y += remainingCross / 2.0f + marginTop; break;
case AlignItems::End:
childLayout.self.y += remainingCross + marginTop; break;
default: break;
}
offsetChildLayouts(childLayout, 0.0f, childLayout.self.y - oldY);
}
}
return result;
}
}

View File

@ -5,145 +5,22 @@
#include "UiPositioner.h"
#include "../UiComponent.h"
#include "../UiText.h"
#include "../../../Application.h"
#include "../../../../../game/ui/components/UiRessourceWidget.h"
#include "../../text/Font.h"
void UiPositioner::applyLayout(const ::LayoutEngine::ComputedLayout &layout) {
screenSpace = layout.self;
for (size_t i = 0; i < uiComponent.children.size(); i++) {
uiComponent.children[i]->uiPositioner.applyLayout(layout.children[i]);
}
}
void UiPositioner::compute(const Dimensions &parent) {
const float screenWidth = static_cast<float>(Application::getInstance().getWindow().GetWidth());
const float screenHeight = static_cast<float>(Application::getInstance().getWindow().GetHeight());
screenSpace.x = parent.x + resolve(style.margin.left, parent.width, screenWidth);
screenSpace.y = parent.y + resolve(style.margin.top, parent.height, screenHeight);
screenSpace.width = resolve(style.width, parent.width, screenWidth);
if (auto text = dynamic_cast<UiText*>(&uiComponent)) {
screenSpace.height = text->getFont().getLineHeight() / screenHeight;
} else {
screenSpace.height = resolve(style.height, parent.height, screenHeight);
}
// 3. Kinder rekursiv positionieren mit JustifyContent
for (auto& child : uiComponent.children) {
child->uiPositioner.compute(screenSpace);
}
float totalChildrenMainSize = 0.0f;
// 3a. Gesamtgröße der Kinder berechnen (inkl. Top/Left Margins)
for (auto& child : uiComponent.children) {
float childMainSize = 0.0f;
float marginMain = 0.0;
if (style.flexDirection == FlexDirection::Column) {
childMainSize = child->uiPositioner.screenSpace.height;
marginMain = child->uiPositioner.resolve(child->uiPositioner.style.margin.top, screenSpace.height, screenHeight);
} else {
childMainSize = child->uiPositioner.screenSpace.width;
marginMain = child->uiPositioner.resolve(child->uiPositioner.style.margin.left, screenSpace.width, screenWidth);
}
totalChildrenMainSize += childMainSize + marginMain;
}
// 3b JustifyContent-offset berechnen
float remainingSpace = ((style.flexDirection == FlexDirection::Column) ? screenSpace.height : screenSpace.width) - totalChildrenMainSize;
float gap =0.0f;
float justifyOffset = 0.0f;
switch (style.justifyContent) {
case JustifyContent::Start: justifyOffset = 0.0f; break;
case JustifyContent::Center: justifyOffset = remainingSpace / 2.0f; break;
case JustifyContent::End: justifyOffset = remainingSpace; break;
case JustifyContent::SpaceBetween:
justifyOffset = 0.0;
if (uiComponent.children.size() > 1) {
gap = remainingSpace / (static_cast<float>(uiComponent.children.size()) - 1.0f);
}
break;
default: break; // Space between later
}
// 3. Kinder rekursiv positionieren
float currentMainOffset = justifyOffset;
bool first = true;
for (auto& child : uiComponent.children) {
if (!first && style.justifyContent == JustifyContent::SpaceBetween) {
currentMainOffset += gap;
}
if (style.flexDirection == FlexDirection::Column) {
child->uiPositioner.offsetPosition(0.f,currentMainOffset);
currentMainOffset += child->uiPositioner.screenSpace.height;
currentMainOffset += child->uiPositioner.resolve(child->uiPositioner.style.margin.top, screenSpace.height, screenHeight);
} else {
child->uiPositioner.offsetPosition(currentMainOffset,0.f);
currentMainOffset += child->uiPositioner.screenSpace.width;
currentMainOffset += child->uiPositioner.resolve(child->uiPositioner.style.margin.left, screenSpace.width, screenWidth);
}
first = false;
}
// AlignItems
for (auto& child : uiComponent.children) {
float oldX = child->uiPositioner.screenSpace.x;
float oldY = child->uiPositioner.screenSpace.y;
if (style.flexDirection == FlexDirection::Column) {
float marginLeft = child->uiPositioner.resolve(child->uiPositioner.style.margin.left,screenSpace.width,screenWidth);
float remainingCrossSpace = screenSpace.width - child->uiPositioner.screenSpace.width ;
switch (style.alignItems) {
case AlignItems::Start:
child->uiPositioner.screenSpace.x = screenSpace.x + marginLeft;
break;
case AlignItems::Center:
child->uiPositioner.screenSpace.x = screenSpace.x + remainingCrossSpace / 2.0f + marginLeft;
break;
case AlignItems::End:
child->uiPositioner.screenSpace.x = screenSpace.x + remainingCrossSpace + marginLeft;
break;
default:
break;
}
float dx = child->uiPositioner.screenSpace.x - oldX;
for (auto& grandchild : child->children) {
grandchild->uiPositioner.offsetPosition(dx,0.0f);
}
} else {
float remainingCrossSpace = screenSpace.height - child->uiPositioner.screenSpace.height;
float marginTop = child->uiPositioner.resolve(child->uiPositioner.style.margin.top,screenSpace.height,screenHeight);
switch (style.alignItems) {
case AlignItems::Start:
child->uiPositioner.screenSpace.y = screenSpace.y + marginTop;
break;
case AlignItems::Center:
child->uiPositioner.screenSpace.y += remainingCrossSpace / 2.0f + marginTop;
break;
case AlignItems::End:
child->uiPositioner.screenSpace.y += remainingCrossSpace + marginTop;
break;
default:
break;
}
float dy = child->uiPositioner.screenSpace.y - oldY;
for (auto& grandchild : child->children) {
grandchild->uiPositioner.offsetPosition(0.0f,dy);
}
}
}
LayoutEngine::NodeLayout node = buildNodeLayout();
LayoutEngine::ComputedLayout result = LayoutEngine::compute(node, parent);
applyLayout(result);
}
LayoutStyle & UiPositioner::getLayout() {
@ -155,6 +32,18 @@ void UiPositioner::setLayout(const LayoutStyle &style) {
}
LayoutEngine::NodeLayout UiPositioner::buildNodeLayout() {
if (uiComponent.children.empty()) {
return {getLayout(), std::vector<LayoutEngine::NodeLayout>()};
}
std::vector<LayoutEngine::NodeLayout> childrenLayouts;
for (const auto& child : uiComponent.children) {
childrenLayouts.push_back(child->uiPositioner.buildNodeLayout());
}
return {getLayout(), childrenLayouts};
}
float UiPositioner::resolve(const SizeValue &size, float parentSize, float axisLength) {
switch (size.unit) {
case SizeUnit::Percent:

View File

@ -7,7 +7,9 @@
#include "../Dimensions.h"
#include <glm/vec2.hpp>
#include "LayoutEngine.h"
#include "LayoutStyle.h"
class UiComponent; // forward declaration
class UiPositioner {
@ -17,6 +19,8 @@ public:
UiPositioner(UiComponent& uiComponent, const LayoutStyle& style): uiComponent(uiComponent), style(style) {}
void applyLayout(const LayoutEngine::ComputedLayout & result);
void compute(const Dimensions& parentDimensions);
LayoutStyle & getLayout();
@ -25,6 +29,7 @@ public:
private:
UiComponent& uiComponent;
LayoutStyle style;
LayoutEngine::NodeLayout buildNodeLayout();
float resolve(const SizeValue &size, float parentSize, float axisLength);
void offsetPosition(float dx, float dy);