445 lines
16 KiB
TypeScript
445 lines
16 KiB
TypeScript
|
|
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
|
|||
|
|
import { UITransformComponent } from '../components/UITransformComponent';
|
|||
|
|
import { UILayoutComponent, UILayoutType, UIJustifyContent, UIAlignItems } from '../components/UILayoutComponent';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* UI 布局系统
|
|||
|
|
* UI Layout System - Computes layout for UI elements
|
|||
|
|
*
|
|||
|
|
* 计算 UI 元素的世界坐标和尺寸
|
|||
|
|
* Computes world coordinates and sizes for UI elements
|
|||
|
|
*/
|
|||
|
|
@ECSSystem('UILayout')
|
|||
|
|
export class UILayoutSystem extends EntitySystem {
|
|||
|
|
/**
|
|||
|
|
* 视口宽度
|
|||
|
|
* Viewport width
|
|||
|
|
*/
|
|||
|
|
public viewportWidth: number = 1920;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 视口高度
|
|||
|
|
* Viewport height
|
|||
|
|
*/
|
|||
|
|
public viewportHeight: number = 1080;
|
|||
|
|
|
|||
|
|
constructor() {
|
|||
|
|
super(Matcher.empty().all(UITransformComponent));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置视口尺寸
|
|||
|
|
* Set viewport size
|
|||
|
|
*/
|
|||
|
|
public setViewport(width: number, height: number): void {
|
|||
|
|
this.viewportWidth = width;
|
|||
|
|
this.viewportHeight = height;
|
|||
|
|
|
|||
|
|
// 标记所有元素需要重新布局
|
|||
|
|
for (const entity of this.entities) {
|
|||
|
|
const transform = entity.getComponent(UITransformComponent);
|
|||
|
|
if (transform) {
|
|||
|
|
transform.layoutDirty = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected process(entities: readonly Entity[]): void {
|
|||
|
|
// 首先处理根元素(没有父元素的)
|
|||
|
|
const rootEntities = entities.filter(e => !e.parent || !e.parent.hasComponent(UITransformComponent));
|
|||
|
|
|
|||
|
|
for (const entity of rootEntities) {
|
|||
|
|
this.layoutEntity(entity, 0, 0, this.viewportWidth, this.viewportHeight, 1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 递归布局实体及其子元素
|
|||
|
|
* Recursively layout entity and its children
|
|||
|
|
*/
|
|||
|
|
private layoutEntity(
|
|||
|
|
entity: Entity,
|
|||
|
|
parentX: number,
|
|||
|
|
parentY: number,
|
|||
|
|
parentWidth: number,
|
|||
|
|
parentHeight: number,
|
|||
|
|
parentAlpha: number
|
|||
|
|
): void {
|
|||
|
|
const transform = entity.getComponent(UITransformComponent);
|
|||
|
|
if (!transform) return;
|
|||
|
|
|
|||
|
|
// 计算锚点位置
|
|||
|
|
const anchorMinX = parentX + parentWidth * transform.anchorMinX;
|
|||
|
|
const anchorMinY = parentY + parentHeight * transform.anchorMinY;
|
|||
|
|
const anchorMaxX = parentX + parentWidth * transform.anchorMaxX;
|
|||
|
|
const anchorMaxY = parentY + parentHeight * transform.anchorMaxY;
|
|||
|
|
|
|||
|
|
// 计算元素尺寸
|
|||
|
|
let width: number;
|
|||
|
|
let height: number;
|
|||
|
|
|
|||
|
|
// 如果锚点 min 和 max 相同,使用固定尺寸
|
|||
|
|
if (transform.anchorMinX === transform.anchorMaxX) {
|
|||
|
|
width = transform.width;
|
|||
|
|
} else {
|
|||
|
|
// 拉伸模式:尺寸由锚点决定
|
|||
|
|
width = anchorMaxX - anchorMinX - transform.x;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (transform.anchorMinY === transform.anchorMaxY) {
|
|||
|
|
height = transform.height;
|
|||
|
|
} else {
|
|||
|
|
height = anchorMaxY - anchorMinY - transform.y;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 应用尺寸约束
|
|||
|
|
if (transform.minWidth > 0) width = Math.max(width, transform.minWidth);
|
|||
|
|
if (transform.maxWidth > 0) width = Math.min(width, transform.maxWidth);
|
|||
|
|
if (transform.minHeight > 0) height = Math.max(height, transform.minHeight);
|
|||
|
|
if (transform.maxHeight > 0) height = Math.min(height, transform.maxHeight);
|
|||
|
|
|
|||
|
|
// 计算世界位置
|
|||
|
|
let worldX: number;
|
|||
|
|
let worldY: number;
|
|||
|
|
|
|||
|
|
if (transform.anchorMinX === transform.anchorMaxX) {
|
|||
|
|
// 固定锚点模式
|
|||
|
|
worldX = anchorMinX + transform.x - width * transform.pivotX;
|
|||
|
|
} else {
|
|||
|
|
// 拉伸模式
|
|||
|
|
worldX = anchorMinX + transform.x;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (transform.anchorMinY === transform.anchorMaxY) {
|
|||
|
|
worldY = anchorMinY + transform.y - height * transform.pivotY;
|
|||
|
|
} else {
|
|||
|
|
worldY = anchorMinY + transform.y;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新计算后的值
|
|||
|
|
transform.worldX = worldX;
|
|||
|
|
transform.worldY = worldY;
|
|||
|
|
transform.computedWidth = width;
|
|||
|
|
transform.computedHeight = height;
|
|||
|
|
transform.worldAlpha = parentAlpha * transform.alpha;
|
|||
|
|
transform.layoutDirty = false;
|
|||
|
|
|
|||
|
|
// 如果元素不可见,跳过子元素
|
|||
|
|
if (!transform.visible) return;
|
|||
|
|
|
|||
|
|
// 处理子元素布局
|
|||
|
|
const children = entity.children.filter(c => c.hasComponent(UITransformComponent));
|
|||
|
|
if (children.length === 0) return;
|
|||
|
|
|
|||
|
|
// 检查是否有布局组件
|
|||
|
|
const layout = entity.getComponent(UILayoutComponent);
|
|||
|
|
if (layout && layout.type !== UILayoutType.None) {
|
|||
|
|
this.layoutChildren(layout, transform, children);
|
|||
|
|
} else {
|
|||
|
|
// 无布局组件,直接递归处理子元素
|
|||
|
|
for (const child of children) {
|
|||
|
|
this.layoutEntity(
|
|||
|
|
child,
|
|||
|
|
worldX,
|
|||
|
|
worldY,
|
|||
|
|
width,
|
|||
|
|
height,
|
|||
|
|
transform.worldAlpha
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 根据布局组件布局子元素
|
|||
|
|
* Layout children according to layout component
|
|||
|
|
*/
|
|||
|
|
private layoutChildren(
|
|||
|
|
layout: UILayoutComponent,
|
|||
|
|
parentTransform: UITransformComponent,
|
|||
|
|
children: Entity[]
|
|||
|
|
): void {
|
|||
|
|
const contentStartX = parentTransform.worldX + layout.paddingLeft;
|
|||
|
|
const contentStartY = parentTransform.worldY + layout.paddingTop;
|
|||
|
|
const contentWidth = parentTransform.computedWidth - layout.getHorizontalPadding();
|
|||
|
|
const contentHeight = parentTransform.computedHeight - layout.getVerticalPadding();
|
|||
|
|
|
|||
|
|
switch (layout.type) {
|
|||
|
|
case UILayoutType.Horizontal:
|
|||
|
|
this.layoutHorizontal(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight);
|
|||
|
|
break;
|
|||
|
|
case UILayoutType.Vertical:
|
|||
|
|
this.layoutVertical(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight);
|
|||
|
|
break;
|
|||
|
|
case UILayoutType.Grid:
|
|||
|
|
this.layoutGrid(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight);
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
// 默认按正常方式递归
|
|||
|
|
for (const child of children) {
|
|||
|
|
this.layoutEntity(
|
|||
|
|
child,
|
|||
|
|
parentTransform.worldX,
|
|||
|
|
parentTransform.worldY,
|
|||
|
|
parentTransform.computedWidth,
|
|||
|
|
parentTransform.computedHeight,
|
|||
|
|
parentTransform.worldAlpha
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 水平布局
|
|||
|
|
* Horizontal layout
|
|||
|
|
*/
|
|||
|
|
private layoutHorizontal(
|
|||
|
|
layout: UILayoutComponent,
|
|||
|
|
parentTransform: UITransformComponent,
|
|||
|
|
children: Entity[],
|
|||
|
|
startX: number,
|
|||
|
|
startY: number,
|
|||
|
|
contentWidth: number,
|
|||
|
|
contentHeight: number
|
|||
|
|
): void {
|
|||
|
|
// 计算总子元素宽度
|
|||
|
|
const childSizes = children.map(child => {
|
|||
|
|
const t = child.getComponent(UITransformComponent)!;
|
|||
|
|
return { entity: child, width: t.width, height: t.height };
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const totalChildWidth = childSizes.reduce((sum, c) => sum + c.width, 0);
|
|||
|
|
const totalGap = layout.gap * (children.length - 1);
|
|||
|
|
const totalWidth = totalChildWidth + totalGap;
|
|||
|
|
|
|||
|
|
// 计算起始位置(基于 justifyContent)
|
|||
|
|
let offsetX = startX;
|
|||
|
|
let gap = layout.gap;
|
|||
|
|
|
|||
|
|
switch (layout.justifyContent) {
|
|||
|
|
case UIJustifyContent.Center:
|
|||
|
|
offsetX = startX + (contentWidth - totalWidth) / 2;
|
|||
|
|
break;
|
|||
|
|
case UIJustifyContent.End:
|
|||
|
|
offsetX = startX + contentWidth - totalWidth;
|
|||
|
|
break;
|
|||
|
|
case UIJustifyContent.SpaceBetween:
|
|||
|
|
if (children.length > 1) {
|
|||
|
|
gap = (contentWidth - totalChildWidth) / (children.length - 1);
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
case UIJustifyContent.SpaceAround:
|
|||
|
|
if (children.length > 0) {
|
|||
|
|
const space = (contentWidth - totalChildWidth) / children.length;
|
|||
|
|
gap = space;
|
|||
|
|
offsetX = startX + space / 2;
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
case UIJustifyContent.SpaceEvenly:
|
|||
|
|
if (children.length > 0) {
|
|||
|
|
const space = (contentWidth - totalChildWidth) / (children.length + 1);
|
|||
|
|
gap = space;
|
|||
|
|
offsetX = startX + space;
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 布局每个子元素
|
|||
|
|
for (let i = 0; i < children.length; i++) {
|
|||
|
|
const child = children[i]!;
|
|||
|
|
const childTransform = child.getComponent(UITransformComponent)!;
|
|||
|
|
const size = childSizes[i]!;
|
|||
|
|
|
|||
|
|
// 计算 Y 位置(基于 alignItems)
|
|||
|
|
let childY = startY;
|
|||
|
|
let childHeight = size.height;
|
|||
|
|
|
|||
|
|
switch (layout.alignItems) {
|
|||
|
|
case UIAlignItems.Center:
|
|||
|
|
childY = startY + (contentHeight - childHeight) / 2;
|
|||
|
|
break;
|
|||
|
|
case UIAlignItems.End:
|
|||
|
|
childY = startY + contentHeight - childHeight;
|
|||
|
|
break;
|
|||
|
|
case UIAlignItems.Stretch:
|
|||
|
|
childHeight = contentHeight;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 直接设置子元素的世界坐标
|
|||
|
|
childTransform.worldX = offsetX;
|
|||
|
|
childTransform.worldY = childY;
|
|||
|
|
childTransform.computedWidth = size.width;
|
|||
|
|
childTransform.computedHeight = childHeight;
|
|||
|
|
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
|
|||
|
|
childTransform.layoutDirty = false;
|
|||
|
|
|
|||
|
|
// 递归处理子元素的子元素
|
|||
|
|
this.processChildrenRecursive(child, childTransform);
|
|||
|
|
|
|||
|
|
offsetX += size.width + gap;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 垂直布局
|
|||
|
|
* Vertical layout
|
|||
|
|
*/
|
|||
|
|
private layoutVertical(
|
|||
|
|
layout: UILayoutComponent,
|
|||
|
|
parentTransform: UITransformComponent,
|
|||
|
|
children: Entity[],
|
|||
|
|
startX: number,
|
|||
|
|
startY: number,
|
|||
|
|
contentWidth: number,
|
|||
|
|
contentHeight: number
|
|||
|
|
): void {
|
|||
|
|
// 计算总子元素高度
|
|||
|
|
const childSizes = children.map(child => {
|
|||
|
|
const t = child.getComponent(UITransformComponent)!;
|
|||
|
|
return { entity: child, width: t.width, height: t.height };
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const totalChildHeight = childSizes.reduce((sum, c) => sum + c.height, 0);
|
|||
|
|
const totalGap = layout.gap * (children.length - 1);
|
|||
|
|
const totalHeight = totalChildHeight + totalGap;
|
|||
|
|
|
|||
|
|
// 计算起始位置
|
|||
|
|
let offsetY = startY;
|
|||
|
|
let gap = layout.gap;
|
|||
|
|
|
|||
|
|
switch (layout.justifyContent) {
|
|||
|
|
case UIJustifyContent.Center:
|
|||
|
|
offsetY = startY + (contentHeight - totalHeight) / 2;
|
|||
|
|
break;
|
|||
|
|
case UIJustifyContent.End:
|
|||
|
|
offsetY = startY + contentHeight - totalHeight;
|
|||
|
|
break;
|
|||
|
|
case UIJustifyContent.SpaceBetween:
|
|||
|
|
if (children.length > 1) {
|
|||
|
|
gap = (contentHeight - totalChildHeight) / (children.length - 1);
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
case UIJustifyContent.SpaceAround:
|
|||
|
|
if (children.length > 0) {
|
|||
|
|
const space = (contentHeight - totalChildHeight) / children.length;
|
|||
|
|
gap = space;
|
|||
|
|
offsetY = startY + space / 2;
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
case UIJustifyContent.SpaceEvenly:
|
|||
|
|
if (children.length > 0) {
|
|||
|
|
const space = (contentHeight - totalChildHeight) / (children.length + 1);
|
|||
|
|
gap = space;
|
|||
|
|
offsetY = startY + space;
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 布局每个子元素
|
|||
|
|
for (let i = 0; i < children.length; i++) {
|
|||
|
|
const child = children[i]!;
|
|||
|
|
const childTransform = child.getComponent(UITransformComponent)!;
|
|||
|
|
const size = childSizes[i]!;
|
|||
|
|
|
|||
|
|
// 计算 X 位置
|
|||
|
|
let childX = startX;
|
|||
|
|
let childWidth = size.width;
|
|||
|
|
|
|||
|
|
switch (layout.alignItems) {
|
|||
|
|
case UIAlignItems.Center:
|
|||
|
|
childX = startX + (contentWidth - childWidth) / 2;
|
|||
|
|
break;
|
|||
|
|
case UIAlignItems.End:
|
|||
|
|
childX = startX + contentWidth - childWidth;
|
|||
|
|
break;
|
|||
|
|
case UIAlignItems.Stretch:
|
|||
|
|
childWidth = contentWidth;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
childTransform.worldX = childX;
|
|||
|
|
childTransform.worldY = offsetY;
|
|||
|
|
childTransform.computedWidth = childWidth;
|
|||
|
|
childTransform.computedHeight = size.height;
|
|||
|
|
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
|
|||
|
|
childTransform.layoutDirty = false;
|
|||
|
|
|
|||
|
|
this.processChildrenRecursive(child, childTransform);
|
|||
|
|
|
|||
|
|
offsetY += size.height + gap;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 网格布局
|
|||
|
|
* Grid layout
|
|||
|
|
*/
|
|||
|
|
private layoutGrid(
|
|||
|
|
layout: UILayoutComponent,
|
|||
|
|
parentTransform: UITransformComponent,
|
|||
|
|
children: Entity[],
|
|||
|
|
startX: number,
|
|||
|
|
startY: number,
|
|||
|
|
contentWidth: number,
|
|||
|
|
_contentHeight: number
|
|||
|
|
): void {
|
|||
|
|
const columns = layout.columns;
|
|||
|
|
const gapX = layout.getHorizontalGap();
|
|||
|
|
const gapY = layout.getVerticalGap();
|
|||
|
|
|
|||
|
|
// 计算单元格尺寸
|
|||
|
|
const cellWidth = layout.cellWidth > 0
|
|||
|
|
? layout.cellWidth
|
|||
|
|
: (contentWidth - gapX * (columns - 1)) / columns;
|
|||
|
|
const cellHeight = layout.cellHeight > 0
|
|||
|
|
? layout.cellHeight
|
|||
|
|
: cellWidth; // 默认正方形
|
|||
|
|
|
|||
|
|
for (let i = 0; i < children.length; i++) {
|
|||
|
|
const child = children[i]!;
|
|||
|
|
const childTransform = child.getComponent(UITransformComponent)!;
|
|||
|
|
|
|||
|
|
const col = i % columns;
|
|||
|
|
const row = Math.floor(i / columns);
|
|||
|
|
|
|||
|
|
const x = startX + col * (cellWidth + gapX);
|
|||
|
|
const y = startY + row * (cellHeight + gapY);
|
|||
|
|
|
|||
|
|
childTransform.worldX = x;
|
|||
|
|
childTransform.worldY = y;
|
|||
|
|
childTransform.computedWidth = cellWidth;
|
|||
|
|
childTransform.computedHeight = cellHeight;
|
|||
|
|
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
|
|||
|
|
childTransform.layoutDirty = false;
|
|||
|
|
|
|||
|
|
this.processChildrenRecursive(child, childTransform);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 递归处理子元素
|
|||
|
|
* Recursively process children
|
|||
|
|
*/
|
|||
|
|
private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent): void {
|
|||
|
|
const children = entity.children.filter(c => c.hasComponent(UITransformComponent));
|
|||
|
|
if (children.length === 0) return;
|
|||
|
|
|
|||
|
|
const layout = entity.getComponent(UILayoutComponent);
|
|||
|
|
if (layout && layout.type !== UILayoutType.None) {
|
|||
|
|
this.layoutChildren(layout, parentTransform, children);
|
|||
|
|
} else {
|
|||
|
|
for (const child of children) {
|
|||
|
|
this.layoutEntity(
|
|||
|
|
child,
|
|||
|
|
parentTransform.worldX,
|
|||
|
|
parentTransform.worldY,
|
|||
|
|
parentTransform.computedWidth,
|
|||
|
|
parentTransform.computedHeight,
|
|||
|
|
parentTransform.worldAlpha
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|