* 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 引用
663 lines
19 KiB
TypeScript
663 lines
19 KiB
TypeScript
import { GComponent } from '../core/GComponent';
|
||
import { GObject } from '../core/GObject';
|
||
import { Controller } from '../core/Controller';
|
||
import { GTextField } from './GTextField';
|
||
import { FGUIEvents } from '../events/Events';
|
||
import { EButtonMode, EObjectPropID } from '../core/FieldTypes';
|
||
import type { ByteBuffer } from '../utils/ByteBuffer';
|
||
|
||
/**
|
||
* GButton
|
||
*
|
||
* Button component with states: up, down, over, selected, disabled.
|
||
*
|
||
* 按钮组件,支持状态:正常、按下、悬停、选中、禁用
|
||
*/
|
||
export class GButton extends GComponent {
|
||
protected _titleObject: GObject | null = null;
|
||
protected _iconObject: GObject | null = null;
|
||
|
||
private _mode: EButtonMode = EButtonMode.Common;
|
||
private _selected: boolean = false;
|
||
private _title: string = '';
|
||
private _selectedTitle: string = '';
|
||
private _icon: string = '';
|
||
private _selectedIcon: string = '';
|
||
private _sound: string = '';
|
||
private _soundVolumeScale: number = 1;
|
||
private _buttonController: Controller | null = null;
|
||
private _relatedController: Controller | null = null;
|
||
private _relatedPageId: string = '';
|
||
private _changeStateOnClick: boolean = true;
|
||
private _linkedPopup: GObject | null = null;
|
||
private _downEffect: number = 0;
|
||
private _downEffectValue: number = 0.8;
|
||
private _downScaled: boolean = false;
|
||
|
||
private _down: boolean = false;
|
||
private _over: boolean = false;
|
||
|
||
public static readonly UP: string = 'up';
|
||
public static readonly DOWN: string = 'down';
|
||
public static readonly OVER: string = 'over';
|
||
public static readonly SELECTED_OVER: string = 'selectedOver';
|
||
public static readonly DISABLED: string = 'disabled';
|
||
public static readonly SELECTED_DISABLED: string = 'selectedDisabled';
|
||
|
||
constructor() {
|
||
super();
|
||
}
|
||
|
||
/**
|
||
* Get/set icon URL
|
||
* 获取/设置图标 URL
|
||
*/
|
||
public get icon(): string {
|
||
return this._icon;
|
||
}
|
||
|
||
public set icon(value: string) {
|
||
this._icon = value;
|
||
const v = this._selected && this._selectedIcon ? this._selectedIcon : this._icon;
|
||
if (this._iconObject) {
|
||
this._iconObject.icon = v;
|
||
}
|
||
this.updateGear(7);
|
||
}
|
||
|
||
/**
|
||
* Get/set selected icon URL
|
||
* 获取/设置选中图标 URL
|
||
*/
|
||
public get selectedIcon(): string {
|
||
return this._selectedIcon;
|
||
}
|
||
|
||
public set selectedIcon(value: string) {
|
||
this._selectedIcon = value;
|
||
const v = this._selected && this._selectedIcon ? this._selectedIcon : this._icon;
|
||
if (this._iconObject) {
|
||
this._iconObject.icon = v;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get/set title text
|
||
* 获取/设置标题文本
|
||
*/
|
||
public get title(): string {
|
||
return this._title;
|
||
}
|
||
|
||
public set title(value: string) {
|
||
this._title = value;
|
||
if (this._titleObject) {
|
||
this._titleObject.text =
|
||
this._selected && this._selectedTitle ? this._selectedTitle : this._title;
|
||
}
|
||
this.updateGear(6);
|
||
}
|
||
|
||
/**
|
||
* Get/set text (alias for title)
|
||
* 获取/设置文本(title 的别名)
|
||
*/
|
||
public get text(): string {
|
||
return this.title;
|
||
}
|
||
|
||
public set text(value: string) {
|
||
this.title = value;
|
||
}
|
||
|
||
/**
|
||
* Get/set selected title text
|
||
* 获取/设置选中标题文本
|
||
*/
|
||
public get selectedTitle(): string {
|
||
return this._selectedTitle;
|
||
}
|
||
|
||
public set selectedTitle(value: string) {
|
||
this._selectedTitle = value;
|
||
if (this._titleObject) {
|
||
this._titleObject.text =
|
||
this._selected && this._selectedTitle ? this._selectedTitle : this._title;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get/set title color
|
||
* 获取/设置标题颜色
|
||
*/
|
||
public get titleColor(): string {
|
||
const tf = this.getTextField();
|
||
if (tf) {
|
||
return tf.color;
|
||
}
|
||
return '#000000';
|
||
}
|
||
|
||
public set titleColor(value: string) {
|
||
const tf = this.getTextField();
|
||
if (tf) {
|
||
tf.color = value;
|
||
}
|
||
this.updateGear(4);
|
||
}
|
||
|
||
/**
|
||
* Get/set title font size
|
||
* 获取/设置标题字体大小
|
||
*/
|
||
public get titleFontSize(): number {
|
||
const tf = this.getTextField();
|
||
if (tf) {
|
||
return tf.fontSize;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
public set titleFontSize(value: number) {
|
||
const tf = this.getTextField();
|
||
if (tf) {
|
||
tf.fontSize = value;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get/set sound URL
|
||
* 获取/设置声音 URL
|
||
*/
|
||
public get sound(): string {
|
||
return this._sound;
|
||
}
|
||
|
||
public set sound(value: string) {
|
||
this._sound = value;
|
||
}
|
||
|
||
/**
|
||
* Get/set sound volume scale
|
||
* 获取/设置声音音量缩放
|
||
*/
|
||
public get soundVolumeScale(): number {
|
||
return this._soundVolumeScale;
|
||
}
|
||
|
||
public set soundVolumeScale(value: number) {
|
||
this._soundVolumeScale = value;
|
||
}
|
||
|
||
/**
|
||
* Get/set selected state
|
||
* 获取/设置选中状态
|
||
*/
|
||
public get selected(): boolean {
|
||
return this._selected;
|
||
}
|
||
|
||
public set selected(value: boolean) {
|
||
if (this._mode === EButtonMode.Common) {
|
||
return;
|
||
}
|
||
|
||
if (this._selected !== value) {
|
||
this._selected = value;
|
||
if (
|
||
this.grayed &&
|
||
this._buttonController &&
|
||
this._buttonController.hasPage(GButton.DISABLED)
|
||
) {
|
||
if (this._selected) {
|
||
this.setState(GButton.SELECTED_DISABLED);
|
||
} else {
|
||
this.setState(GButton.DISABLED);
|
||
}
|
||
} else {
|
||
if (this._selected) {
|
||
this.setState(this._over ? GButton.SELECTED_OVER : GButton.DOWN);
|
||
} else {
|
||
this.setState(this._over ? GButton.OVER : GButton.UP);
|
||
}
|
||
}
|
||
if (this._selectedTitle && this._titleObject) {
|
||
this._titleObject.text = this._selected ? this._selectedTitle : this._title;
|
||
}
|
||
if (this._selectedIcon) {
|
||
const str = this._selected ? this._selectedIcon : this._icon;
|
||
if (this._iconObject) {
|
||
this._iconObject.icon = str;
|
||
}
|
||
}
|
||
if (
|
||
this._relatedController &&
|
||
this._parent &&
|
||
!this._parent._buildingDisplayList
|
||
) {
|
||
if (this._selected) {
|
||
this._relatedController.selectedPageId = this._relatedPageId;
|
||
} else if (
|
||
this._mode === EButtonMode.Check &&
|
||
this._relatedController.selectedPageId === this._relatedPageId
|
||
) {
|
||
// Deselect if in check mode
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get/set button mode
|
||
* 获取/设置按钮模式
|
||
*/
|
||
public get mode(): EButtonMode {
|
||
return this._mode;
|
||
}
|
||
|
||
public set mode(value: EButtonMode) {
|
||
if (this._mode !== value) {
|
||
if (value === EButtonMode.Common) {
|
||
this.selected = false;
|
||
}
|
||
this._mode = value;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get/set related controller
|
||
* 获取/设置关联控制器
|
||
*/
|
||
public get relatedController(): Controller | null {
|
||
return this._relatedController;
|
||
}
|
||
|
||
public set relatedController(value: Controller | null) {
|
||
if (value !== this._relatedController) {
|
||
this._relatedController = value;
|
||
this._relatedPageId = '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get/set related page ID
|
||
* 获取/设置关联页面 ID
|
||
*/
|
||
public get relatedPageId(): string {
|
||
return this._relatedPageId;
|
||
}
|
||
|
||
public set relatedPageId(value: string) {
|
||
this._relatedPageId = value;
|
||
}
|
||
|
||
/**
|
||
* Get/set change state on click
|
||
* 获取/设置点击时是否改变状态
|
||
*/
|
||
public get changeStateOnClick(): boolean {
|
||
return this._changeStateOnClick;
|
||
}
|
||
|
||
public set changeStateOnClick(value: boolean) {
|
||
this._changeStateOnClick = value;
|
||
}
|
||
|
||
/**
|
||
* Get/set linked popup
|
||
* 获取/设置关联弹出窗口
|
||
*/
|
||
public get linkedPopup(): GObject | null {
|
||
return this._linkedPopup;
|
||
}
|
||
|
||
public set linkedPopup(value: GObject | null) {
|
||
this._linkedPopup = value;
|
||
}
|
||
|
||
/**
|
||
* Get text field from title object
|
||
* 从标题对象获取文本字段
|
||
*/
|
||
public getTextField(): GTextField | null {
|
||
if (this._titleObject instanceof GTextField) {
|
||
return this._titleObject;
|
||
} else if (this._titleObject instanceof GButton) {
|
||
return this._titleObject.getTextField();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Fire a click event programmatically
|
||
* 程序化触发点击事件
|
||
*/
|
||
public fireClick(bDownEffect: boolean = true): void {
|
||
if (bDownEffect && this._mode === EButtonMode.Common) {
|
||
this.setState(GButton.OVER);
|
||
setTimeout(() => this.setState(GButton.DOWN), 100);
|
||
setTimeout(() => this.setState(GButton.UP), 200);
|
||
}
|
||
this.handleClick();
|
||
}
|
||
|
||
/**
|
||
* Set button state
|
||
* 设置按钮状态
|
||
*/
|
||
protected setState(value: string): void {
|
||
if (this._buttonController) {
|
||
this._buttonController.selectedPage = value;
|
||
}
|
||
|
||
if (this._downEffect === 1) {
|
||
const cnt = this.numChildren;
|
||
if (
|
||
value === GButton.DOWN ||
|
||
value === GButton.SELECTED_OVER ||
|
||
value === GButton.SELECTED_DISABLED
|
||
) {
|
||
const r = Math.round(this._downEffectValue * 255);
|
||
const color = '#' + ((r << 16) + (r << 8) + r).toString(16).padStart(6, '0');
|
||
for (let i = 0; i < cnt; i++) {
|
||
const obj = this.getChildAt(i);
|
||
if (!(obj instanceof GTextField)) {
|
||
obj.setProp(EObjectPropID.Color, color);
|
||
}
|
||
}
|
||
} else {
|
||
for (let i = 0; i < cnt; i++) {
|
||
const obj = this.getChildAt(i);
|
||
if (!(obj instanceof GTextField)) {
|
||
obj.setProp(EObjectPropID.Color, '#FFFFFF');
|
||
}
|
||
}
|
||
}
|
||
} else if (this._downEffect === 2) {
|
||
if (
|
||
value === GButton.DOWN ||
|
||
value === GButton.SELECTED_OVER ||
|
||
value === GButton.SELECTED_DISABLED
|
||
) {
|
||
if (!this._downScaled) {
|
||
this.setScale(
|
||
this.scaleX * this._downEffectValue,
|
||
this.scaleY * this._downEffectValue
|
||
);
|
||
this._downScaled = true;
|
||
}
|
||
} else {
|
||
if (this._downScaled) {
|
||
this.setScale(
|
||
this.scaleX / this._downEffectValue,
|
||
this.scaleY / this._downEffectValue
|
||
);
|
||
this._downScaled = false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public handleControllerChanged(c: Controller): void {
|
||
super.handleControllerChanged(c);
|
||
|
||
if (this._relatedController === c) {
|
||
this.selected = this._relatedPageId === c.selectedPageId;
|
||
}
|
||
}
|
||
|
||
protected handleGrayedChanged(): void {
|
||
if (
|
||
this._buttonController &&
|
||
this._buttonController.hasPage(GButton.DISABLED)
|
||
) {
|
||
if (this.grayed) {
|
||
if (
|
||
this._selected &&
|
||
this._buttonController.hasPage(GButton.SELECTED_DISABLED)
|
||
) {
|
||
this.setState(GButton.SELECTED_DISABLED);
|
||
} else {
|
||
this.setState(GButton.DISABLED);
|
||
}
|
||
} else if (this._selected) {
|
||
this.setState(GButton.DOWN);
|
||
} else {
|
||
this.setState(GButton.UP);
|
||
}
|
||
} else {
|
||
super.handleGrayedChanged();
|
||
}
|
||
}
|
||
|
||
public getProp(index: number): any {
|
||
switch (index) {
|
||
case EObjectPropID.Color:
|
||
return this.titleColor;
|
||
case EObjectPropID.OutlineColor:
|
||
const tf = this.getTextField();
|
||
if (tf) {
|
||
return tf.strokeColor;
|
||
}
|
||
return '#000000';
|
||
case EObjectPropID.FontSize:
|
||
return this.titleFontSize;
|
||
case EObjectPropID.Selected:
|
||
return this.selected;
|
||
default:
|
||
return super.getProp(index);
|
||
}
|
||
}
|
||
|
||
public setProp(index: number, value: any): void {
|
||
switch (index) {
|
||
case EObjectPropID.Color:
|
||
this.titleColor = value;
|
||
break;
|
||
case EObjectPropID.OutlineColor:
|
||
const tf = this.getTextField();
|
||
if (tf) {
|
||
tf.strokeColor = value;
|
||
}
|
||
break;
|
||
case EObjectPropID.FontSize:
|
||
this.titleFontSize = value;
|
||
break;
|
||
case EObjectPropID.Selected:
|
||
this.selected = value;
|
||
break;
|
||
default:
|
||
super.setProp(index, value);
|
||
break;
|
||
}
|
||
}
|
||
|
||
protected constructExtension(buffer: ByteBuffer): void {
|
||
buffer.seek(0, 6);
|
||
|
||
this._mode = buffer.readByte();
|
||
const str = buffer.readS();
|
||
if (str) {
|
||
this._sound = str;
|
||
}
|
||
this._soundVolumeScale = buffer.getFloat32();
|
||
this._downEffect = buffer.readByte();
|
||
this._downEffectValue = buffer.getFloat32();
|
||
if (this._downEffect === 2) {
|
||
this.setPivot(0.5, 0.5, this.pivotAsAnchor);
|
||
}
|
||
|
||
this._buttonController = this.getController('button');
|
||
this._titleObject = this.getChild('title');
|
||
this._iconObject = this.getChild('icon');
|
||
if (this._titleObject) {
|
||
this._title = this._titleObject.text || '';
|
||
}
|
||
if (this._iconObject) {
|
||
this._icon = this._iconObject.icon || '';
|
||
}
|
||
|
||
if (this._mode === EButtonMode.Common) {
|
||
this.setState(GButton.UP);
|
||
}
|
||
|
||
this.on(FGUIEvents.ROLL_OVER, this.handleRollOver, this);
|
||
this.on(FGUIEvents.ROLL_OUT, this.handleRollOut, this);
|
||
this.on(FGUIEvents.TOUCH_BEGIN, this.handleTouchBegin, this);
|
||
this.on(FGUIEvents.CLICK, this.handleClick, this);
|
||
}
|
||
|
||
public setup_afterAdd(buffer: ByteBuffer, beginPos: number): void {
|
||
super.setup_afterAdd(buffer, beginPos);
|
||
|
||
if (!buffer.seek(beginPos, 6)) {
|
||
return;
|
||
}
|
||
|
||
if (buffer.readByte() !== this.packageItem?.objectType) {
|
||
return;
|
||
}
|
||
|
||
let str = buffer.readS();
|
||
if (str) {
|
||
this.title = str;
|
||
}
|
||
str = buffer.readS();
|
||
if (str) {
|
||
this.selectedTitle = str;
|
||
}
|
||
str = buffer.readS();
|
||
if (str) {
|
||
this.icon = str;
|
||
}
|
||
str = buffer.readS();
|
||
if (str) {
|
||
this.selectedIcon = str;
|
||
}
|
||
if (buffer.readBool()) {
|
||
this.titleColor = buffer.readS();
|
||
}
|
||
const iv = buffer.getInt32();
|
||
if (iv !== 0) {
|
||
this.titleFontSize = iv;
|
||
}
|
||
const controllerIndex = buffer.getInt16();
|
||
if (controllerIndex >= 0 && this.parent) {
|
||
this._relatedController = this.parent.getControllerAt(controllerIndex);
|
||
}
|
||
this._relatedPageId = buffer.readS();
|
||
|
||
str = buffer.readS();
|
||
if (str) {
|
||
this._sound = str;
|
||
}
|
||
if (buffer.readBool()) {
|
||
this._soundVolumeScale = buffer.getFloat32();
|
||
}
|
||
|
||
this.selected = buffer.readBool();
|
||
}
|
||
|
||
private handleRollOver(): void {
|
||
if (
|
||
!this._buttonController ||
|
||
!this._buttonController.hasPage(GButton.OVER)
|
||
) {
|
||
return;
|
||
}
|
||
|
||
this._over = true;
|
||
if (this._down) {
|
||
return;
|
||
}
|
||
|
||
if (this.grayed && this._buttonController.hasPage(GButton.DISABLED)) {
|
||
return;
|
||
}
|
||
|
||
this.setState(this._selected ? GButton.SELECTED_OVER : GButton.OVER);
|
||
}
|
||
|
||
private handleRollOut(): void {
|
||
if (
|
||
!this._buttonController ||
|
||
!this._buttonController.hasPage(GButton.OVER)
|
||
) {
|
||
return;
|
||
}
|
||
|
||
this._over = false;
|
||
if (this._down) {
|
||
return;
|
||
}
|
||
|
||
if (this.grayed && this._buttonController.hasPage(GButton.DISABLED)) {
|
||
return;
|
||
}
|
||
|
||
this.setState(this._selected ? GButton.DOWN : GButton.UP);
|
||
}
|
||
|
||
private handleTouchBegin(): void {
|
||
this._down = true;
|
||
|
||
if (this._mode === EButtonMode.Common) {
|
||
if (
|
||
this.grayed &&
|
||
this._buttonController &&
|
||
this._buttonController.hasPage(GButton.DISABLED)
|
||
) {
|
||
this.setState(GButton.SELECTED_DISABLED);
|
||
} else {
|
||
this.setState(GButton.DOWN);
|
||
}
|
||
}
|
||
|
||
// Listen for touch end globally
|
||
this.root?.on(FGUIEvents.TOUCH_END, this.handleTouchEnd, this);
|
||
}
|
||
|
||
private handleTouchEnd(): void {
|
||
if (this._down) {
|
||
this.root?.off(FGUIEvents.TOUCH_END, this.handleTouchEnd, this);
|
||
this._down = false;
|
||
|
||
if (!this._displayObject) {
|
||
return;
|
||
}
|
||
|
||
if (this._mode === EButtonMode.Common) {
|
||
if (
|
||
this.grayed &&
|
||
this._buttonController &&
|
||
this._buttonController.hasPage(GButton.DISABLED)
|
||
) {
|
||
this.setState(GButton.DISABLED);
|
||
} else if (this._over) {
|
||
this.setState(GButton.OVER);
|
||
} else {
|
||
this.setState(GButton.UP);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private handleClick(): void {
|
||
if (this._mode === EButtonMode.Check) {
|
||
if (this._changeStateOnClick) {
|
||
this.selected = !this._selected;
|
||
this.emit(FGUIEvents.STATE_CHANGED);
|
||
}
|
||
} else if (this._mode === EButtonMode.Radio) {
|
||
if (this._changeStateOnClick && !this._selected) {
|
||
this.selected = true;
|
||
this.emit(FGUIEvents.STATE_CHANGED);
|
||
}
|
||
} else {
|
||
if (this._relatedController) {
|
||
this._relatedController.selectedPageId = this._relatedPageId;
|
||
}
|
||
}
|
||
}
|
||
}
|