Files
esengine/packages/fairygui/src/core/GComponent.ts

1006 lines
28 KiB
TypeScript
Raw Normal View History

feat(fairygui): FairyGUI 完整集成 (#314) * feat(fairygui): FairyGUI ECS 集成核心架构 实现 FairyGUI 的 ECS 原生集成,完全替代旧 UI 系统: 核心类: - GObject: UI 对象基类,支持变换、可见性、关联、齿轮 - GComponent: 容器组件,管理子对象和控制器 - GRoot: 根容器,管理焦点、弹窗、输入分发 - GGroup: 组容器,支持水平/垂直布局 抽象层: - DisplayObject: 显示对象基类 - EventDispatcher: 事件分发 - Timer: 计时器 - Stage: 舞台,管理输入和缩放 布局系统: - Relations: 约束关联管理 - RelationItem: 24 种关联类型 基础设施: - Controller: 状态控制器 - Transition: 过渡动画 - ScrollPane: 滚动面板 - UIPackage: 包管理 - ByteBuffer: 二进制解析 * refactor(ui): 删除旧 UI 系统,使用 FairyGUI 替代 * feat(fairygui): 实现 UI 控件 - 添加显示类:Image、TextField、Graph - 添加基础控件:GImage、GTextField、GGraph - 添加交互控件:GButton、GProgressBar、GSlider - 更新 IRenderCollector 支持 Graph 渲染 - 扩展 Controller 添加 selectedPageId - 添加 STATE_CHANGED 事件类型 * feat(fairygui): 现代化架构重构 - 增强 EventDispatcher 支持类型安全、优先级和传播控制 - 添加 PropertyBinding 响应式属性绑定系统 - 添加 ServiceContainer 依赖注入容器 - 添加 UIConfig 全局配置系统 - 添加 UIObjectFactory 对象工厂 - 实现 RenderBridge 渲染桥接层 - 实现 Canvas2DBackend 作为默认渲染后端 - 扩展 IRenderCollector 支持更多图元类型 * feat(fairygui): 九宫格渲染和资源加载修复 - 修复 FGUIUpdateSystem 支持路径和 GUID 两种加载方式 - 修复 GTextInput 同时设置 _displayObject 和 _textField - 实现九宫格渲染展开为 9 个子图元 - 添加 sourceWidth/sourceHeight 用于九宫格计算 - 添加 DOMTextRenderer 文本渲染层(临时方案) * fix(fairygui): 修复 GGraph 颜色读取 * feat(fairygui): 虚拟节点 Inspector 和文本渲染支持 * fix(fairygui): 编辑器状态刷新和遗留引用修复 - 修复切换 FGUI 包后组件列表未刷新问题 - 修复切换组件后 viewport 未清理旧内容问题 - 修复虚拟节点在包加载后未刷新问题 - 重构为事件驱动架构,移除轮询机制 - 修复 @esengine/ui 遗留引用,统一使用 @esengine/fairygui * fix: 移除 tsconfig 中的 @esengine/ui 引用
2025-12-22 10:52:54 +08:00
import { GObject } from './GObject';
import { GGroup } from './GGroup';
import { Controller } from './Controller';
import { Transition } from './Transition';
import { UIObjectFactory } from './UIObjectFactory';
import { EOverflowType, EChildrenRenderOrder } from './FieldTypes';
import { Rectangle, Margin } from '../utils/MathTypes';
import { Container } from '../display/Container';
import type { ScrollPane } from '../scroll/ScrollPane';
import type { IRenderCollector } from '../render/IRenderCollector';
import type { ByteBuffer } from '../utils/ByteBuffer';
import type { PackageItem } from '../package/PackageItem';
/**
* GComponent
*
* Container class that can hold children.
* Supports controllers, transitions, scroll, and masking.
*
*
*/
export class GComponent extends GObject {
/** Opaque hit area | 不透明点击区域 */
public opaque: boolean = true;
protected _margin: Margin = new Margin();
protected _trackBounds: boolean = false;
protected _boundsChanged: boolean = false;
protected _childrenRenderOrder: EChildrenRenderOrder = EChildrenRenderOrder.Ascent;
protected _apexIndex: number = 0;
protected _children: GObject[] = [];
protected _controllers: Controller[] = [];
protected _transitions: Transition[] = [];
protected _scrollPane: ScrollPane | null = null;
public _buildingDisplayList: boolean = false;
protected _sortingChildCount: number = 0;
// Overflow and masking | 溢出和遮罩
protected _overflow: EOverflowType = EOverflowType.Visible;
protected _clipRect: Rectangle | null = null;
constructor() {
super();
}
/**
* Create display object for this component
*
*/
protected createDisplayObject(): void {
this._displayObject = new Container();
this._displayObject.gOwner = this;
}
// Children management | 子对象管理
/**
* Get child count
*
*/
public get numChildren(): number {
return this._children.length;
}
/**
* Add a child
*
*/
public addChild(child: GObject): GObject {
return this.addChildAt(child, this._children.length);
}
/**
* Add child at specific index
*
*/
public addChildAt(child: GObject, index: number): GObject {
if (!child) {
throw new Error('child is null');
}
const count = this._children.length;
if (index < 0 || index > count) {
throw new Error('Invalid child index: ' + index);
}
if (child._parent === this) {
this.setChildIndex(child, index);
} else {
child.removeFromParent();
child._parent = this;
if (this._sortingChildCount > 0) {
this._sortingChildCount++;
index = this.getInsertPosForSortingChild(child);
}
this._children.splice(index, 0, child);
this.childStateChanged(child);
this.setBoundsChangedFlag();
}
return child;
}
/**
* Remove a child
*
*/
public removeChild(child: GObject, bDispose: boolean = false): GObject {
const index = this._children.indexOf(child);
if (index !== -1) {
return this.removeChildAt(index, bDispose);
}
return child;
}
/**
* Remove child at index
*
*/
public removeChildAt(index: number, bDispose: boolean = false): GObject {
if (index < 0 || index >= this._children.length) {
throw new Error('Invalid child index: ' + index);
}
const child = this._children[index];
child._parent = null;
if (child.sortingOrder !== 0) {
this._sortingChildCount--;
}
this._children.splice(index, 1);
if (child.internalVisible) {
const childDisplay = child.displayObject;
if (this._displayObject && childDisplay) {
this._displayObject.removeChild(childDisplay);
}
}
this.setBoundsChangedFlag();
if (bDispose) {
child.dispose();
}
return child;
}
/**
* Remove children in range
*
*/
public removeChildren(beginIndex: number = 0, endIndex: number = -1, bDispose: boolean = false): void {
if (endIndex < 0 || endIndex >= this._children.length) {
endIndex = this._children.length - 1;
}
for (let i = endIndex; i >= beginIndex; i--) {
this.removeChildAt(i, bDispose);
}
}
/**
* Get child at index
*
*/
public getChildAt(index: number): GObject {
if (index < 0 || index >= this._children.length) {
throw new Error('Invalid child index: ' + index);
}
return this._children[index];
}
/**
* Get child by name
*
*/
public getChild(name: string): GObject | null {
for (const child of this._children) {
if (child.name === name) {
return child;
}
}
return null;
}
/**
* Get visible child by name
*
*/
public getVisibleChild(name: string): GObject | null {
for (const child of this._children) {
if (child.name === name && child.internalVisible) {
return child;
}
}
return null;
}
/**
* Get child by ID
* ID
*/
public getChildById(id: string): GObject | null {
for (const child of this._children) {
if (child.id === id) {
return child;
}
}
return null;
}
/**
* Get child by path
*
*/
public getChildByPath(path: string): GObject | null {
const arr = path.split('.');
let obj: GObject | null = this;
for (const part of arr) {
if (obj instanceof GComponent) {
obj = obj.getChild(part);
} else {
return null;
}
}
return obj;
}
/**
* Get child index
*
*/
public getChildIndex(child: GObject): number {
return this._children.indexOf(child);
}
/**
* Set child index
*
*/
public setChildIndex(child: GObject, index: number): void {
const oldIndex = this._children.indexOf(child);
if (oldIndex === -1) {
throw new Error('Not a child of this container');
}
if (child.sortingOrder !== 0) {
return;
}
const count = this._children.length;
if (this._sortingChildCount > 0) {
if (index > count - this._sortingChildCount - 1) {
index = count - this._sortingChildCount - 1;
}
}
this.moveChild(child, oldIndex, index);
}
/**
* Set child index before another child
*
*/
public setChildIndexBefore(child: GObject, beforeChild: GObject): number {
const index = this._children.indexOf(child);
if (index === -1) {
throw new Error('Not a child of this container');
}
let beforeIndex = this._children.indexOf(beforeChild);
if (beforeIndex === -1) {
throw new Error('beforeChild is not a child of this container');
}
if (child.sortingOrder !== 0 || beforeChild.sortingOrder !== 0) {
return index;
}
if (index < beforeIndex) {
beforeIndex--;
}
this.moveChild(child, index, beforeIndex);
return beforeIndex;
}
private moveChild(child: GObject, oldIndex: number, newIndex: number): void {
this._children.splice(oldIndex, 1);
this._children.splice(newIndex, 0, child);
const childDisplay = child.displayObject;
if (this._displayObject && childDisplay && child.internalVisible) {
// Update display list order
this._displayObject.setChildIndex(childDisplay, newIndex);
}
this.setBoundsChangedFlag();
}
/**
* Swap children positions
*
*/
public swapChildren(child1: GObject, child2: GObject): void {
const index1 = this._children.indexOf(child1);
const index2 = this._children.indexOf(child2);
if (index1 === -1 || index2 === -1) {
throw new Error('Not a child of this container');
}
this.swapChildrenAt(index1, index2);
}
/**
* Swap children at positions
*
*/
public swapChildrenAt(index1: number, index2: number): void {
const child1 = this._children[index1];
const child2 = this._children[index2];
this.setChildIndex(child1, index2);
this.setChildIndex(child2, index1);
}
/**
* Check if contains a child
*
*/
public isChildInView(child: GObject): boolean {
if (this._scrollPane) {
return this._scrollPane.isChildInView(child);
}
if (this._clipRect) {
return child.x + child.width > 0 &&
child.x < this._width &&
child.y + child.height > 0 &&
child.y < this._height;
}
return true;
}
/**
* Get children in a group
*
*/
public getChildrenInGroup(group: GGroup): GObject[] {
const result: GObject[] = [];
for (const child of this._children) {
if (child.group === group) {
result.push(child);
}
}
return result;
}
private getInsertPosForSortingChild(target: GObject): number {
const count = this._children.length;
let i: number;
for (i = 0; i < count; i++) {
const child = this._children[i];
if (child === target) continue;
if (target.sortingOrder < child.sortingOrder) break;
}
return i;
}
// Controller management | 控制器管理
/**
* Get controller count
*
*/
public get numControllers(): number {
return this._controllers.length;
}
/**
* Get controller at index
*
*/
public getControllerAt(index: number): Controller {
return this._controllers[index];
}
/**
* Get controller by name
*
*/
public getController(name: string): Controller | null {
for (const c of this._controllers) {
if (c.name === name) {
return c;
}
}
return null;
}
/**
* Add controller
*
*/
public addController(controller: Controller): void {
this._controllers.push(controller);
controller.parent = this;
this.applyController(controller);
}
/**
* Remove controller
*
*/
public removeController(controller: Controller): void {
const index = this._controllers.indexOf(controller);
if (index !== -1) {
controller.parent = null;
this._controllers.splice(index, 1);
for (const child of this._children) {
child.handleControllerChanged(controller);
}
}
}
/**
* Apply controller changes
*
*/
public applyController(controller: Controller): void {
for (const child of this._children) {
child.handleControllerChanged(controller);
}
}
/**
* Apply all controllers
*
*/
public applyAllControllers(): void {
for (const c of this._controllers) {
this.applyController(c);
}
}
// Transition management | 过渡动画管理
/**
* Get transition at index
*
*/
public getTransitionAt(index: number): Transition {
return this._transitions[index];
}
/**
* Get transition by name
*
*/
public getTransition(name: string): Transition | null {
for (const t of this._transitions) {
if (t.name === name) {
return t;
}
}
return null;
}
// Scroll pane | 滚动面板
public get scrollPane(): ScrollPane | null {
return this._scrollPane;
}
// Overflow | 溢出
public get overflow(): EOverflowType {
return this._overflow;
}
public set overflow(value: EOverflowType) {
if (this._overflow !== value) {
this._overflow = value;
if (value === EOverflowType.Hidden) {
this._clipRect = new Rectangle(0, 0, this._width, this._height);
} else if (value === EOverflowType.Visible) {
this._clipRect = null;
}
}
}
// Children render order | 子对象渲染顺序
public get childrenRenderOrder(): EChildrenRenderOrder {
return this._childrenRenderOrder;
}
public set childrenRenderOrder(value: EChildrenRenderOrder) {
if (this._childrenRenderOrder !== value) {
this._childrenRenderOrder = value;
this.buildNativeDisplayList();
}
}
public get apexIndex(): number {
return this._apexIndex;
}
public set apexIndex(value: number) {
if (this._apexIndex !== value) {
this._apexIndex = value;
if (this._childrenRenderOrder === EChildrenRenderOrder.Arch) {
this.buildNativeDisplayList();
}
}
}
// Bounds management | 边界管理
/**
* Set bounds changed flag
*
*/
public setBoundsChangedFlag(): void {
if (!this._boundsChanged) {
this._boundsChanged = true;
}
}
/**
* Ensure bounds are correct
*
*/
public ensureBoundsCorrect(): void {
if (this._boundsChanged) {
this.updateBounds();
}
}
protected updateBounds(): void {
let ax = 0, ay = 0, aw = 0, ah = 0;
for (const child of this._children) {
const ar = child.x + child.actualWidth;
const ab = child.y + child.actualHeight;
if (ar > aw) aw = ar;
if (ab > ah) ah = ab;
}
this.setBounds(ax, ay, aw, ah);
}
public setBounds(ax: number, ay: number, aw: number, ah: number): void {
this._boundsChanged = false;
}
// Child state | 子对象状态
/**
* Notify child state changed
*
*/
public childStateChanged(child: GObject): void {
if (this._buildingDisplayList) return;
const childDisplay = child.displayObject;
if (child.internalVisible) {
if (this._displayObject && childDisplay) {
if (childDisplay.parent !== this._displayObject) {
const index = this.getChildIndex(child);
this._displayObject.addChildAt(childDisplay, index);
}
}
} else {
if (this._displayObject && childDisplay) {
this._displayObject.removeChild(childDisplay);
}
}
}
/**
* Notify child sorting order changed
*
*/
public childSortingOrderChanged(child: GObject, oldValue: number, newValue: number): void {
if (newValue === 0) {
this._sortingChildCount--;
this.setChildIndex(child, this._children.length);
} else {
if (oldValue === 0) {
this._sortingChildCount++;
}
const oldIndex = this._children.indexOf(child);
const newIndex = this.getInsertPosForSortingChild(child);
if (oldIndex < newIndex) {
this.moveChild(child, oldIndex, newIndex - 1);
} else {
this.moveChild(child, oldIndex, newIndex);
}
}
}
// Display list building | 构建显示列表
protected buildNativeDisplayList(): void {
if (!this._displayObject) return;
this._buildingDisplayList = true;
const count = this._children.length;
if (count === 0) {
this._buildingDisplayList = false;
return;
}
switch (this._childrenRenderOrder) {
case EChildrenRenderOrder.Ascent:
for (let i = 0; i < count; i++) {
const child = this._children[i];
const childDisplay = child.displayObject;
if (child.internalVisible && childDisplay) {
this._displayObject.addChild(childDisplay);
}
}
break;
case EChildrenRenderOrder.Descent:
for (let i = count - 1; i >= 0; i--) {
const child = this._children[i];
const childDisplay = child.displayObject;
if (child.internalVisible && childDisplay) {
this._displayObject.addChild(childDisplay);
}
}
break;
case EChildrenRenderOrder.Arch:
for (let i = 0; i < this._apexIndex; i++) {
const child = this._children[i];
const childDisplay = child.displayObject;
if (child.internalVisible && childDisplay) {
this._displayObject.addChild(childDisplay);
}
}
for (let i = count - 1; i >= this._apexIndex; i--) {
const child = this._children[i];
const childDisplay = child.displayObject;
if (child.internalVisible && childDisplay) {
this._displayObject.addChild(childDisplay);
}
}
break;
}
this._buildingDisplayList = false;
}
// Setup methods | 设置方法
/**
* Setup scroll pane from buffer
*
*/
protected setupScroll(_buffer: ByteBuffer): void {
// ScrollPane setup will be implemented when needed
// For now, create a basic scroll pane
const { ScrollPane } = require('../scroll/ScrollPane');
this._scrollPane = new ScrollPane(this);
}
/**
* Setup overflow behavior
*
*/
protected setupOverflow(overflow: EOverflowType): void {
this._overflow = overflow;
if (overflow === EOverflowType.Hidden) {
this._clipRect = new Rectangle(0, 0, this._width, this._height);
}
}
// Construction from resource | 从资源构建
/**
* Construct from resource
*
*/
public constructFromResource(): void {
this.constructFromResource2(null, 0);
}
/**
* Construct from resource with object pool
* 使
*/
public constructFromResource2(objectPool: GObject[] | null, poolIndex: number): void {
if (!this.packageItem) {
console.warn('[GComponent] constructFromResource2: packageItem is null');
return;
}
const contentItem = this.packageItem.getBranch ? this.packageItem.getBranch() : this.packageItem;
if (!contentItem.rawData) {
console.warn('[GComponent] constructFromResource2: rawData is null for', contentItem.name);
return;
}
const buffer = contentItem.rawData;
buffer.seek(0, 0);
this._underConstruct = true;
this.sourceWidth = buffer.getInt32();
this.sourceHeight = buffer.getInt32();
this.initWidth = this.sourceWidth;
this.initHeight = this.sourceHeight;
this.setSize(this.sourceWidth, this.sourceHeight);
if (buffer.readBool()) {
this.minWidth = buffer.getInt32();
this.maxWidth = buffer.getInt32();
this.minHeight = buffer.getInt32();
this.maxHeight = buffer.getInt32();
}
if (buffer.readBool()) {
const f1 = buffer.getFloat32();
const f2 = buffer.getFloat32();
this.internalSetPivot(f1, f2, buffer.readBool());
}
if (buffer.readBool()) {
this._margin.top = buffer.getInt32();
this._margin.bottom = buffer.getInt32();
this._margin.left = buffer.getInt32();
this._margin.right = buffer.getInt32();
}
const overflow = buffer.readByte() as EOverflowType;
if (overflow === EOverflowType.Scroll) {
const savedPos = buffer.pos;
buffer.seek(0, 7);
this.setupScroll(buffer);
buffer.pos = savedPos;
} else {
this.setupOverflow(overflow);
}
if (buffer.readBool()) {
buffer.skip(8); // Skip custom data
}
this._buildingDisplayList = true;
// Read controllers
buffer.seek(0, 1);
const controllerCount = buffer.getInt16();
for (let i = 0; i < controllerCount; i++) {
let nextPos = buffer.getInt16();
nextPos += buffer.pos;
const controller = new Controller();
this._controllers.push(controller);
controller.parent = this;
controller.setup(buffer);
buffer.pos = nextPos;
}
// Read children
buffer.seek(0, 2);
const childCount = buffer.getInt16();
for (let i = 0; i < childCount; i++) {
const dataLen = buffer.getInt16();
const curPos = buffer.pos;
let child: GObject;
if (objectPool) {
child = objectPool[poolIndex + i];
} else {
buffer.seek(curPos, 0);
const type = buffer.readByte();
const src = buffer.readS();
const pkgId = buffer.readS();
let pi: PackageItem | null = null;
// Note: readS() returns '' for null/empty, so check for non-empty string
if (src && contentItem.owner) {
const pkg = pkgId
? contentItem.owner.getPackageById(pkgId)
: contentItem.owner;
pi = pkg ? pkg.getItemById(src) : null;
} else {
}
if (pi) {
child = UIObjectFactory.newObject(pi);
child.constructFromResource();
} else {
child = UIObjectFactory.newObject(type);
}
}
child._underConstruct = true;
child.setup_beforeAdd(buffer, curPos);
child._parent = this;
this._children.push(child);
buffer.pos = curPos + dataLen;
}
// Setup relations for this component
buffer.seek(0, 3);
this.relations.setup(buffer, true);
// Setup relations for children
buffer.seek(0, 2);
buffer.skip(2); // Skip child count
for (let i = 0; i < childCount; i++) {
let nextPos = buffer.getInt16();
nextPos += buffer.pos;
buffer.seek(buffer.pos, 3);
this._children[i].relations.setup(buffer, false);
buffer.pos = nextPos;
}
// Call setup_afterAdd for children
buffer.seek(0, 2);
buffer.skip(2); // Skip child count
for (let i = 0; i < childCount; i++) {
let nextPos = buffer.getInt16();
nextPos += buffer.pos;
const child = this._children[i];
child.setup_afterAdd(buffer, buffer.pos);
child._underConstruct = false;
buffer.pos = nextPos;
}
// Read custom properties
buffer.seek(0, 4);
buffer.skip(2); // customData
this.opaque = buffer.readBool();
const maskId = buffer.getInt16();
if (maskId !== -1) {
// Mask setup - skip for now
buffer.readBool(); // inverted mask
}
// Hit test - skip for now
buffer.readS(); // hitTestId
buffer.getInt32(); // i1
buffer.getInt32(); // i2
// Read transitions
buffer.seek(0, 5);
const transitionCount = buffer.getInt16();
for (let i = 0; i < transitionCount; i++) {
let nextPos = buffer.getInt16();
nextPos += buffer.pos;
const trans = new Transition(this);
trans.setup(buffer);
this._transitions.push(trans);
buffer.pos = nextPos;
}
// Apply all controllers
if (this._transitions.length > 0) {
for (const trans of this._transitions) {
if (trans.autoPlay) {
trans.play();
}
}
}
this.applyAllControllers();
this._buildingDisplayList = false;
this._underConstruct = false;
this.buildNativeDisplayList();
this.setBoundsChangedFlag();
if (contentItem.objectType !== undefined) {
this.constructExtension(buffer);
}
this.onConstruct();
}
/**
* Internal set pivot
*
*/
protected internalSetPivot(xv: number, yv: number, bAsAnchor: boolean): void {
this._pivotX = xv;
this._pivotY = yv;
this._pivotAsAnchor = bAsAnchor;
}
/**
* Construct extension (override in subclasses)
*
*/
protected constructExtension(_buffer: ByteBuffer): void {
// Override in subclasses
}
/**
* Called after construction is complete
*
*/
protected onConstruct(): void {
// Override in subclasses
}
// Size handling | 尺寸处理
protected handleSizeChanged(): void {
super.handleSizeChanged();
if (this._clipRect) {
this._clipRect.width = this._width;
this._clipRect.height = this._height;
}
if (this._scrollPane) {
this._scrollPane.onOwnerSizeChanged();
}
}
// Disposal | 销毁
public dispose(): void {
for (const t of this._transitions) {
t.dispose();
}
this._transitions.length = 0;
for (const c of this._controllers) {
c.dispose();
}
this._controllers.length = 0;
if (this._scrollPane) {
this._scrollPane.dispose();
this._scrollPane = null;
}
this.removeChildren(0, -1, true);
super.dispose();
}
// Render data collection | 渲染数据收集
public collectRenderData(collector: IRenderCollector): void {
if (!this._visible) return;
// Update this component's display object transform
// 更新此组件的显示对象变换
if (this._displayObject) {
this._displayObject.updateTransform();
}
if (this._clipRect) {
collector.pushClipRect(this._clipRect);
}
for (const child of this._children) {
if (child.internalVisible) {
child.collectRenderData(collector);
}
}
if (this._clipRect) {
collector.popClipRect();
}
}
}