Files
esengine/packages/fairygui/src/widgets/GButton.ts

663 lines
19 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 { 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;
}
}
}
}