Files
esengine/packages/fairygui/src/widgets/GButton.ts
YHH a1e1189f9d 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

663 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}
}