refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,662 @@
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;
}
}
}
}

View File

@@ -0,0 +1,330 @@
import { GObject } from '../core/GObject';
import { Graph } from '../display/Graph';
import { EGraphType, EObjectPropID } from '../core/FieldTypes';
import type { ByteBuffer } from '../utils/ByteBuffer';
/**
* GGraph - FairyGUI 图形显示对象
*
* Supports rect, ellipse, polygon, and regular polygon shapes.
* 支持矩形、椭圆、多边形和正多边形
*/
export class GGraph extends GObject {
private _graph!: Graph;
private _type: EGraphType = EGraphType.Empty;
private _lineSize: number = 1;
private _lineColor: string = '#000000';
private _fillColor: string = '#FFFFFF';
private _cornerRadius: number[] | null = null;
private _sides: number = 3;
private _startAngle: number = 0;
private _polygonPoints: number[] | null = null;
private _distances: number[] | null = null;
constructor() {
super();
this.ensureGraph();
}
private ensureGraph(): void {
if (!this._graph) {
this.createDisplayObject();
}
}
protected createDisplayObject(): void {
this._displayObject = this._graph = new Graph();
this._graph.touchable = false;
this._displayObject.gOwner = this;
}
public get type(): EGraphType {
return this._type;
}
public get polygonPoints(): number[] | null {
return this._polygonPoints;
}
public get fillColor(): string {
return this._fillColor;
}
public set fillColor(value: string) {
if (value === this._fillColor) return;
this._fillColor = value;
this.updateGraph();
}
public get lineColor(): string {
return this._lineColor;
}
public set lineColor(value: string) {
if (value === this._lineColor) return;
this._lineColor = value;
this.updateGraph();
}
public get color(): string {
return this._fillColor;
}
public set color(value: string) {
this._fillColor = value;
this.updateGear(4);
if (this._type !== EGraphType.Empty) {
this.updateGraph();
}
}
public get distances(): number[] | null {
return this._distances;
}
public set distances(value: number[] | null) {
this._distances = value;
if (this._type === EGraphType.RegularPolygon) {
this.updateGraph();
}
}
public drawRect(
lineSize: number,
lineColor: string,
fillColor: string,
cornerRadius?: number[]
): void {
this._type = EGraphType.Rect;
this._lineSize = lineSize;
this._lineColor = lineColor;
this._fillColor = fillColor;
this._cornerRadius = cornerRadius || null;
this.updateGraph();
}
public drawEllipse(lineSize: number, lineColor: string, fillColor: string): void {
this._type = EGraphType.Ellipse;
this._lineSize = lineSize;
this._lineColor = lineColor;
this._fillColor = fillColor;
this.updateGraph();
}
public drawRegularPolygon(
lineSize: number,
lineColor: string,
fillColor: string,
sides: number,
startAngle?: number,
distances?: number[]
): void {
this._type = EGraphType.RegularPolygon;
this._lineSize = lineSize;
this._lineColor = lineColor;
this._fillColor = fillColor;
this._sides = sides;
this._startAngle = startAngle || 0;
this._distances = distances || null;
this.updateGraph();
}
public drawPolygon(
lineSize: number,
lineColor: string,
fillColor: string,
points: number[]
): void {
this._type = EGraphType.Polygon;
this._lineSize = lineSize;
this._lineColor = lineColor;
this._fillColor = fillColor;
this._polygonPoints = points;
this.updateGraph();
}
private updateGraph(): void {
this.ensureGraph();
if (!this._graph) return;
this._graph.touchable = this.touchable;
const w = this.width;
const h = this.height;
if (w === 0 || h === 0) {
this._graph.clear();
return;
}
switch (this._type) {
case EGraphType.Rect:
this._graph.drawRect(
this._lineSize,
this._lineColor,
this._fillColor,
this._cornerRadius || undefined
);
break;
case EGraphType.Ellipse:
this._graph.drawEllipse(this._lineSize, this._lineColor, this._fillColor);
break;
case EGraphType.Polygon:
if (this._polygonPoints) {
this._graph.drawPolygon(
this._lineSize,
this._lineColor,
this._fillColor,
this._polygonPoints
);
}
break;
case EGraphType.RegularPolygon:
this.generateRegularPolygonPoints();
if (this._polygonPoints) {
this._graph.drawPolygon(
this._lineSize,
this._lineColor,
this._fillColor,
this._polygonPoints
);
}
break;
default:
this._graph.clear();
break;
}
this._graph.width = w;
this._graph.height = h;
}
private generateRegularPolygonPoints(): void {
const radius = Math.min(this._width, this._height) / 2;
this._polygonPoints = [];
const angle = (this._startAngle * Math.PI) / 180;
const deltaAngle = (2 * Math.PI) / this._sides;
for (let i = 0; i < this._sides; i++) {
let dist = 1;
if (this._distances && this._distances[i] !== undefined) {
dist = this._distances[i];
if (isNaN(dist)) dist = 1;
}
const xv = radius + radius * dist * Math.cos(angle + deltaAngle * i);
const yv = radius + radius * dist * Math.sin(angle + deltaAngle * i);
this._polygonPoints.push(xv, yv);
}
}
public replaceMe(target: GObject): void {
if (!this._parent) {
throw new Error('parent not set');
}
target.name = this.name;
target.alpha = this.alpha;
target.rotation = this.rotation;
target.visible = this.visible;
target.touchable = this.touchable;
target.grayed = this.grayed;
target.setXY(this.x, this.y);
target.setSize(this.width, this.height);
const index = this._parent.getChildIndex(this);
this._parent.addChildAt(target, index);
target.relations.copyFrom(this.relations);
this._parent.removeChild(this, true);
}
public addBeforeMe(target: GObject): void {
if (!this._parent) {
throw new Error('parent not set');
}
const index = this._parent.getChildIndex(this);
this._parent.addChildAt(target, index);
}
/**
* Add target after this object
* 在此对象后添加目标
*/
public addAfterMe(target: GObject): void {
if (!this._parent) {
throw new Error('parent not set');
}
const index = this._parent.getChildIndex(this);
this._parent.addChildAt(target, index + 1);
}
public getProp(index: number): any {
if (index === EObjectPropID.Color) {
return this.color;
}
return super.getProp(index);
}
public setProp(index: number, value: any): void {
if (index === EObjectPropID.Color) {
this.color = value;
} else {
super.setProp(index, value);
}
}
protected handleSizeChanged(): void {
super.handleSizeChanged();
if (this._type !== EGraphType.Empty) {
this.updateGraph();
}
}
public setup_beforeAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_beforeAdd(buffer, beginPos);
buffer.seek(beginPos, 5);
this._type = buffer.readByte();
if (this._type !== EGraphType.Empty) {
this._lineSize = buffer.getInt32();
this._lineColor = buffer.readColorS(true);
this._fillColor = buffer.readColorS(true);
if (buffer.readBool()) {
this._cornerRadius = [];
for (let i = 0; i < 4; i++) {
this._cornerRadius[i] = buffer.getFloat32();
}
}
if (this._type === EGraphType.Polygon) {
const cnt = buffer.getInt16();
this._polygonPoints = [];
for (let i = 0; i < cnt; i++) {
this._polygonPoints[i] = buffer.getFloat32();
}
} else if (this._type === EGraphType.RegularPolygon) {
this._sides = buffer.getInt16();
this._startAngle = buffer.getFloat32();
const cnt = buffer.getInt16();
if (cnt > 0) {
this._distances = [];
for (let i = 0; i < cnt; i++) {
this._distances[i] = buffer.getFloat32();
}
}
}
}
}
public setup_afterAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_afterAdd(buffer, beginPos);
if (this._type !== EGraphType.Empty) {
this.updateGraph();
}
}
}

View File

@@ -0,0 +1,232 @@
import { GObject } from '../core/GObject';
import { Image } from '../display/Image';
import { Rectangle } from '../utils/MathTypes';
import { EFlipType, EFillMethod, EObjectPropID } from '../core/FieldTypes';
import type { ByteBuffer } from '../utils/ByteBuffer';
import type { PackageItem } from '../package/PackageItem';
/**
* GImage
*
* Image display object for FairyGUI.
*
* FairyGUI 图像显示对象
*/
export class GImage extends GObject {
private _image!: Image;
private _flip: EFlipType = EFlipType.None;
private _contentItem: PackageItem | null = null;
constructor() {
super();
// Ensure _image is initialized - super() calls createDisplayObject() but
// class field initializers run after super(), which may cause issues
this.ensureImage();
}
private ensureImage(): void {
if (!this._image) {
this.createDisplayObject();
}
}
/**
* Get the internal image display object
* 获取内部图像显示对象
*/
public get image(): Image {
return this._image;
}
/**
* Get/set color tint
* 获取/设置颜色着色
*/
public get color(): string {
return this._image.color;
}
public set color(value: string) {
if (this._image && this._image.color !== value) {
this._image.color = value;
this.updateGear(4);
}
}
/**
* Get/set flip type
* 获取/设置翻转类型
*/
public get flip(): EFlipType {
return this._flip;
}
public set flip(value: EFlipType) {
if (this._flip !== value) {
this._flip = value;
let sx = 1;
let sy = 1;
if (this._flip === EFlipType.Horizontal || this._flip === EFlipType.Both) {
sx = -1;
}
if (this._flip === EFlipType.Vertical || this._flip === EFlipType.Both) {
sy = -1;
}
this.setScale(sx, sy);
this.handleXYChanged();
}
}
/**
* Get/set fill method
* 获取/设置填充方法
*/
public get fillMethod(): EFillMethod {
return this._image.fillMethod;
}
public set fillMethod(value: EFillMethod) {
if (this._image) {
this._image.fillMethod = value;
}
}
/**
* Get/set fill origin
* 获取/设置填充起点
*/
public get fillOrigin(): number {
return this._image.fillOrigin;
}
public set fillOrigin(value: number) {
if (this._image) {
this._image.fillOrigin = value;
}
}
/**
* Get/set fill clockwise
* 获取/设置填充顺时针方向
*/
public get fillClockwise(): boolean {
return this._image.fillClockwise;
}
public set fillClockwise(value: boolean) {
if (this._image) {
this._image.fillClockwise = value;
}
}
/**
* Get/set fill amount (0-1)
* 获取/设置填充量0-1
*/
public get fillAmount(): number {
return this._image.fillAmount;
}
public set fillAmount(value: number) {
if (this._image) {
this._image.fillAmount = value;
}
}
protected createDisplayObject(): void {
this._displayObject = this._image = new Image();
this._image.touchable = false;
this._displayObject.gOwner = this;
}
/**
* Construct from package resource
* 从包资源构建
*/
public constructFromResource(): void {
if (!this.packageItem) return;
this.ensureImage();
this._contentItem = this.packageItem;
this.sourceWidth = this._contentItem.width;
this.sourceHeight = this._contentItem.height;
this.initWidth = this.sourceWidth;
this.initHeight = this.sourceHeight;
this._image.scale9Grid = this._contentItem.scale9Grid
? new Rectangle(
this._contentItem.scale9Grid.x,
this._contentItem.scale9Grid.y,
this._contentItem.scale9Grid.width,
this._contentItem.scale9Grid.height
)
: null;
this._image.scaleByTile = this._contentItem.scaleByTile;
this._image.tileGridIndice = this._contentItem.tileGridIndice;
// Load texture from package (this decodes the sprite info)
if (this._contentItem.owner) {
this._contentItem.owner.getItemAsset(this._contentItem);
}
this._image.texture = this._contentItem.texture;
this.setSize(this.sourceWidth, this.sourceHeight);
}
protected handleXYChanged(): void {
super.handleXYChanged();
if (this._flip !== EFlipType.None) {
if (this.scaleX === -1 && this._image) {
this._image.x += this.width;
}
if (this.scaleY === -1 && this._image) {
this._image.y += this.height;
}
}
}
public getProp(index: number): any {
if (index === EObjectPropID.Color) {
return this.color;
}
return super.getProp(index);
}
public setProp(index: number, value: any): void {
if (index === EObjectPropID.Color) {
this.color = value;
} else {
super.setProp(index, value);
}
}
public setup_beforeAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_beforeAdd(buffer, beginPos);
buffer.seek(beginPos, 5);
if (buffer.readBool()) {
this.color = buffer.readS();
}
this.flip = buffer.readByte();
const fillMethodValue = buffer.readByte();
if (this._image) {
this._image.fillMethod = fillMethodValue;
if (this._image.fillMethod !== EFillMethod.None) {
this._image.fillOrigin = buffer.readByte();
this._image.fillClockwise = buffer.readBool();
this._image.fillAmount = buffer.getFloat32();
}
} else if (fillMethodValue !== EFillMethod.None) {
// Skip bytes if _image not ready
buffer.readByte(); // fillOrigin
buffer.readBool(); // fillClockwise
buffer.getFloat32(); // fillAmount
}
}
}

View File

@@ -0,0 +1,750 @@
import { GComponent } from '../core/GComponent';
import { GObject } from '../core/GObject';
import { GObjectPool } from '../core/GObjectPool';
import { GButton } from './GButton';
import { Controller } from '../core/Controller';
import { UIPackage } from '../package/UIPackage';
import { FGUIEvents } from '../events/Events';
import { Point, Margin } from '../utils/MathTypes';
import {
EListLayoutType,
EListSelectionMode,
EChildrenRenderOrder,
EOverflowType,
EAlignType,
EVertAlignType
} from '../core/FieldTypes';
import type { ByteBuffer } from '../utils/ByteBuffer';
/**
* Item renderer callback
* 项渲染回调
*/
export type ItemRenderer = (index: number, item: GObject) => void;
/**
* Item provider callback
* 项提供者回调
*/
export type ItemProvider = (index: number) => string;
/**
* GList
*
* Scrollable list component with item pooling support.
*
* 带有项池化支持的可滚动列表组件
*
* Features:
* - Multiple layout modes (single column/row, flow, pagination)
* - Item selection (single, multiple)
* - Object pooling for performance
*/
export class GList extends GComponent {
/** Item renderer callback | 项渲染回调 */
public itemRenderer: ItemRenderer | null = null;
/** Item provider callback | 项提供者回调 */
public itemProvider: ItemProvider | null = null;
/** Scroll item to view on click | 点击时滚动项到视图 */
public scrollItemToViewOnClick: boolean = true;
/** Fold invisible items | 折叠不可见项 */
public foldInvisibleItems: boolean = false;
private _layout: EListLayoutType = EListLayoutType.SingleColumn;
private _lineCount: number = 0;
private _columnCount: number = 0;
private _lineGap: number = 0;
private _columnGap: number = 0;
private _defaultItem: string = '';
private _autoResizeItem: boolean = true;
private _selectionMode: EListSelectionMode = EListSelectionMode.Single;
private _align: EAlignType = EAlignType.Left;
private _verticalAlign: EVertAlignType = EVertAlignType.Top;
private _selectionController: Controller | null = null;
private _lastSelectedIndex: number = -1;
private _pool: GObjectPool;
private _listMargin: Margin = new Margin();
constructor() {
super();
this._pool = new GObjectPool();
this._trackBounds = true;
}
public dispose(): void {
this._pool.clear();
super.dispose();
}
// Layout properties
public get layout(): EListLayoutType {
return this._layout;
}
public set layout(value: EListLayoutType) {
if (this._layout !== value) {
this._layout = value;
this.setBoundsChangedFlag();
}
}
public get lineCount(): number {
return this._lineCount;
}
public set lineCount(value: number) {
if (this._lineCount !== value) {
this._lineCount = value;
this.setBoundsChangedFlag();
}
}
public get columnCount(): number {
return this._columnCount;
}
public set columnCount(value: number) {
if (this._columnCount !== value) {
this._columnCount = value;
this.setBoundsChangedFlag();
}
}
public get lineGap(): number {
return this._lineGap;
}
public set lineGap(value: number) {
if (this._lineGap !== value) {
this._lineGap = value;
this.setBoundsChangedFlag();
}
}
public get columnGap(): number {
return this._columnGap;
}
public set columnGap(value: number) {
if (this._columnGap !== value) {
this._columnGap = value;
this.setBoundsChangedFlag();
}
}
public get align(): EAlignType {
return this._align;
}
public set align(value: EAlignType) {
if (this._align !== value) {
this._align = value;
this.setBoundsChangedFlag();
}
}
public get verticalAlign(): EVertAlignType {
return this._verticalAlign;
}
public set verticalAlign(value: EVertAlignType) {
if (this._verticalAlign !== value) {
this._verticalAlign = value;
this.setBoundsChangedFlag();
}
}
public get defaultItem(): string {
return this._defaultItem;
}
public set defaultItem(value: string) {
this._defaultItem = UIPackage.normalizeURL(value);
}
public get autoResizeItem(): boolean {
return this._autoResizeItem;
}
public set autoResizeItem(value: boolean) {
if (this._autoResizeItem !== value) {
this._autoResizeItem = value;
this.setBoundsChangedFlag();
}
}
public get selectionMode(): EListSelectionMode {
return this._selectionMode;
}
public set selectionMode(value: EListSelectionMode) {
this._selectionMode = value;
}
public get selectionController(): Controller | null {
return this._selectionController;
}
public set selectionController(value: Controller | null) {
this._selectionController = value;
}
public get itemPool(): GObjectPool {
return this._pool;
}
// Item pool operations
public getFromPool(url?: string): GObject | null {
if (!url) {
url = this._defaultItem;
}
const obj = this._pool.getObject(url);
if (obj) {
obj.visible = true;
}
return obj;
}
public returnToPool(obj: GObject): void {
this._pool.returnObject(obj);
}
// Item operations
public addChildAt(child: GObject, index: number): GObject {
super.addChildAt(child, index);
if (child instanceof GButton) {
child.selected = false;
child.changeStateOnClick = false;
}
child.on(FGUIEvents.CLICK, this._onClickItem, this);
return child;
}
public addItem(url?: string): GObject | null {
if (!url) {
url = this._defaultItem;
}
const obj = UIPackage.createObjectFromURL(url);
if (obj) {
return this.addChild(obj);
}
return null;
}
public addItemFromPool(url?: string): GObject | null {
const obj = this.getFromPool(url);
if (obj) {
return this.addChild(obj);
}
return null;
}
public removeChildAt(index: number, bDispose?: boolean): GObject {
const child = super.removeChildAt(index, bDispose);
if (!bDispose) {
child.off(FGUIEvents.CLICK, this._onClickItem, this);
}
return child;
}
public removeChildToPoolAt(index: number): void {
const child = super.removeChildAt(index);
this.returnToPool(child);
}
public removeChildToPool(child: GObject): void {
super.removeChild(child);
this.returnToPool(child);
}
public removeChildrenToPool(beginIndex: number = 0, endIndex: number = -1): void {
if (endIndex < 0 || endIndex >= this.numChildren) {
endIndex = this.numChildren - 1;
}
for (let i = beginIndex; i <= endIndex; ++i) {
this.removeChildToPoolAt(beginIndex);
}
}
// Selection
public get selectedIndex(): number {
const cnt = this.numChildren;
for (let i = 0; i < cnt; i++) {
const obj = this.getChildAt(i);
if (obj instanceof GButton && obj.selected) {
return i;
}
}
return -1;
}
public set selectedIndex(value: number) {
if (value >= 0 && value < this.numChildren) {
if (this._selectionMode !== EListSelectionMode.Single) {
this.clearSelection();
}
this.addSelection(value);
} else {
this.clearSelection();
}
}
public getSelection(result?: number[]): number[] {
if (!result) {
result = [];
}
const cnt = this.numChildren;
for (let i = 0; i < cnt; i++) {
const obj = this.getChildAt(i);
if (obj instanceof GButton && obj.selected) {
result.push(i);
}
}
return result;
}
public addSelection(index: number, bScrollItToView?: boolean): void {
if (this._selectionMode === EListSelectionMode.None) {
return;
}
if (this._selectionMode === EListSelectionMode.Single) {
this.clearSelection();
}
if (bScrollItToView) {
this.scrollToView(index);
}
this._lastSelectedIndex = index;
const obj = this.getChildAt(index);
if (obj instanceof GButton && !obj.selected) {
obj.selected = true;
this.updateSelectionController(index);
}
}
public removeSelection(index: number): void {
if (this._selectionMode === EListSelectionMode.None) {
return;
}
const obj = this.getChildAt(index);
if (obj instanceof GButton) {
obj.selected = false;
}
}
public clearSelection(): void {
const cnt = this.numChildren;
for (let i = 0; i < cnt; i++) {
const obj = this.getChildAt(i);
if (obj instanceof GButton) {
obj.selected = false;
}
}
}
public selectAll(): void {
let last = -1;
const cnt = this.numChildren;
for (let i = 0; i < cnt; i++) {
const obj = this.getChildAt(i);
if (obj instanceof GButton && !obj.selected) {
obj.selected = true;
last = i;
}
}
if (last !== -1) {
this.updateSelectionController(last);
}
}
public selectNone(): void {
this.clearSelection();
}
public selectReverse(): void {
let last = -1;
const cnt = this.numChildren;
for (let i = 0; i < cnt; i++) {
const obj = this.getChildAt(i);
if (obj instanceof GButton) {
obj.selected = !obj.selected;
if (obj.selected) {
last = i;
}
}
}
if (last !== -1) {
this.updateSelectionController(last);
}
}
// Scroll
public scrollToView(index: number, bAni?: boolean, bSetFirst?: boolean): void {
const obj = this.getChildAt(index);
if (obj && this._scrollPane) {
this._scrollPane.scrollToView(obj, bAni, bSetFirst);
}
}
// Item count
public get numItems(): number {
return this.numChildren;
}
public set numItems(value: number) {
const cnt = this.numChildren;
if (value > cnt) {
for (let i = cnt; i < value; i++) {
if (this.itemProvider) {
this.addItemFromPool(this.itemProvider(i));
} else {
this.addItemFromPool();
}
}
} else {
this.removeChildrenToPool(value, cnt);
}
if (this.itemRenderer) {
for (let i = 0; i < value; i++) {
const child = this.getChildAt(i);
if (child) {
this.itemRenderer(i, child);
}
}
}
}
// Size
public resizeToFit(itemCount?: number, minSize?: number): void {
if (itemCount == null) itemCount = 100000;
if (minSize == null) minSize = 0;
this.ensureBoundsCorrect();
let curCount = this.numItems;
if (itemCount > curCount) {
itemCount = curCount;
}
if (itemCount === 0) {
if (this._layout === EListLayoutType.SingleColumn ||
this._layout === EListLayoutType.FlowHorizontal) {
this.viewHeight = minSize;
} else {
this.viewWidth = minSize;
}
} else {
let i = itemCount - 1;
let obj: GObject | null = null;
while (i >= 0) {
obj = this.getChildAt(i);
if (!this.foldInvisibleItems || obj?.visible) {
break;
}
i--;
}
if (i < 0 || !obj) {
if (this._layout === EListLayoutType.SingleColumn ||
this._layout === EListLayoutType.FlowHorizontal) {
this.viewHeight = minSize;
} else {
this.viewWidth = minSize;
}
} else {
let size = 0;
if (this._layout === EListLayoutType.SingleColumn ||
this._layout === EListLayoutType.FlowHorizontal) {
size = obj.y + obj.height;
if (size < minSize) size = minSize;
this.viewHeight = size;
} else {
size = obj.x + obj.width;
if (size < minSize) size = minSize;
this.viewWidth = size;
}
}
}
}
public getMaxItemWidth(): number {
const cnt = this.numChildren;
let max = 0;
for (let i = 0; i < cnt; i++) {
const child = this.getChildAt(i);
if (child && child.width > max) {
max = child.width;
}
}
return max;
}
// View size helpers
public get viewWidth(): number {
if (this._scrollPane) {
return this._scrollPane.viewWidth;
}
return this.width - this._listMargin.left - this._listMargin.right;
}
public set viewWidth(value: number) {
if (this._scrollPane) {
// Adjust component width
}
this.width = value + this._listMargin.left + this._listMargin.right;
}
public get viewHeight(): number {
if (this._scrollPane) {
return this._scrollPane.viewHeight;
}
return this.height - this._listMargin.top - this._listMargin.bottom;
}
public set viewHeight(value: number) {
if (this._scrollPane) {
// Adjust component height
}
this.height = value + this._listMargin.top + this._listMargin.bottom;
}
protected handleSizeChanged(): void {
super.handleSizeChanged();
this.setBoundsChangedFlag();
}
public handleControllerChanged(c: Controller): void {
super.handleControllerChanged(c);
if (this._selectionController === c) {
this.selectedIndex = c.selectedIndex;
}
}
// Event handlers
private _onClickItem(item: GObject): void {
if (this._scrollPane && this._scrollPane.isDragged) {
return;
}
this.setSelectionOnEvent(item);
if (this._scrollPane && this.scrollItemToViewOnClick) {
this._scrollPane.scrollToView(item, true);
}
this.emit(FGUIEvents.CLICK_ITEM, item);
}
private setSelectionOnEvent(item: GObject): void {
if (!(item instanceof GButton) || this._selectionMode === EListSelectionMode.None) {
return;
}
const index = this.getChildIndex(item);
if (this._selectionMode === EListSelectionMode.Single) {
if (!item.selected) {
this.clearSelectionExcept(item);
item.selected = true;
}
} else {
if (this._selectionMode === EListSelectionMode.MultipleSingleClick) {
item.selected = !item.selected;
} else {
if (!item.selected) {
this.clearSelectionExcept(item);
item.selected = true;
} else {
this.clearSelectionExcept(item);
}
}
}
this._lastSelectedIndex = index;
if (item.selected) {
this.updateSelectionController(index);
}
}
private clearSelectionExcept(g: GObject): void {
const cnt = this.numChildren;
for (let i = 0; i < cnt; i++) {
const obj = this.getChildAt(i);
if (obj instanceof GButton && obj !== g) {
obj.selected = false;
}
}
}
private updateSelectionController(index: number): void {
if (this._selectionController && !this._selectionController.changing &&
index < this._selectionController.pageCount) {
const c = this._selectionController;
this._selectionController = null;
c.selectedIndex = index;
this._selectionController = c;
}
}
// Setup from buffer
public setup_beforeAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_beforeAdd(buffer, beginPos);
buffer.seek(beginPos, 5);
this._layout = buffer.readByte();
this._selectionMode = buffer.readByte();
const i1 = buffer.readByte();
this._align = i1 === 0 ? EAlignType.Left : (i1 === 1 ? EAlignType.Center : EAlignType.Right);
const i2 = buffer.readByte();
this._verticalAlign = i2 === 0 ? EVertAlignType.Top : (i2 === 1 ? EVertAlignType.Middle : EVertAlignType.Bottom);
this._lineGap = buffer.getInt16();
this._columnGap = buffer.getInt16();
this._lineCount = buffer.getInt16();
this._columnCount = buffer.getInt16();
this._autoResizeItem = buffer.readBool();
this._childrenRenderOrder = buffer.readByte() as EChildrenRenderOrder;
this._apexIndex = buffer.getInt16();
if (buffer.readBool()) {
this._listMargin.top = buffer.getInt32();
this._listMargin.bottom = buffer.getInt32();
this._listMargin.left = buffer.getInt32();
this._listMargin.right = buffer.getInt32();
}
const overflow = buffer.readByte() as EOverflowType;
if (overflow === EOverflowType.Scroll) {
const savedPos = buffer.position;
buffer.seek(beginPos, 7);
this.setupScroll(buffer);
buffer.position = savedPos;
} else {
this.setupOverflow(overflow);
}
if (buffer.readBool()) {
buffer.skip(8); // clipSoftness
}
if (buffer.version >= 2) {
this.scrollItemToViewOnClick = buffer.readBool();
this.foldInvisibleItems = buffer.readBool();
}
buffer.seek(beginPos, 8);
this._defaultItem = buffer.readS();
this.readItems(buffer);
}
protected readItems(buffer: ByteBuffer): void {
const cnt = buffer.getInt16();
for (let i = 0; i < cnt; i++) {
const nextPos = buffer.getInt16() + buffer.position;
let str = buffer.readS();
if (!str) {
str = this._defaultItem;
if (!str) {
buffer.position = nextPos;
continue;
}
}
const obj = this.getFromPool(str);
if (obj) {
this.addChild(obj);
this.setupItem(buffer, obj);
}
buffer.position = nextPos;
}
}
protected setupItem(buffer: ByteBuffer, obj: GObject): void {
let str = buffer.readS();
if (str) {
obj.text = str;
}
str = buffer.readS();
if (str && obj instanceof GButton) {
obj.selectedTitle = str;
}
str = buffer.readS();
if (str) {
obj.icon = str;
}
str = buffer.readS();
if (str && obj instanceof GButton) {
obj.selectedIcon = str;
}
str = buffer.readS();
if (str) {
obj.name = str;
}
if (obj instanceof GComponent) {
const cnt = buffer.getInt16();
for (let i = 0; i < cnt; i++) {
const cc = obj.getController(buffer.readS());
const pageId = buffer.readS();
if (cc) {
cc.selectedPageId = pageId;
}
}
if (buffer.version >= 2) {
const cnt2 = buffer.getInt16();
for (let i = 0; i < cnt2; i++) {
const target = buffer.readS();
const propertyId = buffer.getInt16();
const value = buffer.readS();
const obj2 = obj.getChildByPath(target);
if (obj2) {
obj2.setProp(propertyId, value);
}
}
}
}
}
public setup_afterAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_afterAdd(buffer, beginPos);
buffer.seek(beginPos, 6);
const i = buffer.getInt16();
if (i !== -1 && this._parent) {
this._selectionController = this._parent.getControllerAt(i);
}
}
}

View File

@@ -0,0 +1,741 @@
import { GObject } from '../core/GObject';
import { GComponent } from '../core/GComponent';
import { GObjectPool } from '../core/GObjectPool';
import { MovieClip, type IFrame } from '../display/MovieClip';
import { Container } from '../display/Container';
import type { ISpriteTexture } from '../display/Image';
import { UIPackage } from '../package/UIPackage';
import { getUIConfig } from '../core/UIConfig';
import {
ELoaderFillType,
EAlignType,
EVertAlignType,
EPackageItemType,
EObjectPropID,
EFillMethod
} from '../core/FieldTypes';
import { Rectangle } from '../utils/MathTypes';
import type { ByteBuffer } from '../utils/ByteBuffer';
import type { PackageItem } from '../package/PackageItem';
/**
* GLoader
*
* Content loader component for loading images, movie clips, and components.
* Supports various fill modes, alignment, and automatic sizing.
*
* 内容加载器组件,用于加载图像、动画和组件
* 支持多种填充模式、对齐方式和自动尺寸
*
* Features:
* - Load images from package or external URL
* - Load movie clips (animations)
* - Load components as content
* - Multiple fill modes (none, scale, fit, etc.)
* - Alignment control
* - Error sign display
*/
export class GLoader extends GObject {
private _url: string = '';
private _align: EAlignType = EAlignType.Left;
private _valign: EVertAlignType = EVertAlignType.Top;
private _autoSize: boolean = false;
private _fill: ELoaderFillType = ELoaderFillType.None;
private _shrinkOnly: boolean = false;
private _useResize: boolean = false;
private _showErrorSign: boolean = true;
private _contentItem: PackageItem | null = null;
private _content!: MovieClip;
private _errorSign: GObject | null = null;
private _content2: GComponent | null = null;
private _updatingLayout: boolean = false;
private static _errorSignPool: GObjectPool = new GObjectPool();
constructor() {
super();
}
protected createDisplayObject(): void {
this._displayObject = new Container();
this._displayObject.gOwner = this;
this._displayObject.touchable = true;
this._content = new MovieClip();
this._displayObject.addChild(this._content);
}
public dispose(): void {
if (!this._contentItem && this._content?.texture) {
this.freeExternal(this._content.texture);
}
if (this._content2) {
this._content2.dispose();
this._content2 = null;
}
super.dispose();
}
/**
* Get/set resource URL
* 获取/设置资源 URL
*/
public get url(): string {
return this._url;
}
public set url(value: string) {
if (this._url === value) return;
this._url = value;
this.loadContent();
this.updateGear(7);
}
/**
* Icon alias for url
* URL 的图标别名
*/
public get icon(): string {
return this._url;
}
public set icon(value: string) {
this.url = value;
}
/**
* Get/set horizontal alignment
* 获取/设置水平对齐
*/
public get align(): EAlignType {
return this._align;
}
public set align(value: EAlignType) {
if (this._align !== value) {
this._align = value;
this.updateLayout();
}
}
/**
* Get/set vertical alignment
* 获取/设置垂直对齐
*/
public get verticalAlign(): EVertAlignType {
return this._valign;
}
public set verticalAlign(value: EVertAlignType) {
if (this._valign !== value) {
this._valign = value;
this.updateLayout();
}
}
/**
* Get/set fill type
* 获取/设置填充类型
*/
public get fill(): ELoaderFillType {
return this._fill;
}
public set fill(value: ELoaderFillType) {
if (this._fill !== value) {
this._fill = value;
this.updateLayout();
}
}
/**
* Get/set shrink only mode
* 获取/设置仅缩小模式
*/
public get shrinkOnly(): boolean {
return this._shrinkOnly;
}
public set shrinkOnly(value: boolean) {
if (this._shrinkOnly !== value) {
this._shrinkOnly = value;
this.updateLayout();
}
}
/**
* Get/set use resize mode
* 获取/设置使用 resize 模式
*/
public get useResize(): boolean {
return this._useResize;
}
public set useResize(value: boolean) {
if (this._useResize !== value) {
this._useResize = value;
this.updateLayout();
}
}
/**
* Get/set auto size mode
* 获取/设置自动尺寸模式
*/
public get autoSize(): boolean {
return this._autoSize;
}
public set autoSize(value: boolean) {
if (this._autoSize !== value) {
this._autoSize = value;
this.updateLayout();
}
}
/**
* Get/set playing state (for movie clips)
* 获取/设置播放状态(用于动画)
*/
public get playing(): boolean {
return this._content?.playing ?? false;
}
public set playing(value: boolean) {
if (this._content && this._content.playing !== value) {
this._content.playing = value;
this.updateGear(5);
}
}
/**
* Get/set current frame (for movie clips)
* 获取/设置当前帧(用于动画)
*/
public get frame(): number {
return this._content?.frame ?? 0;
}
public set frame(value: number) {
if (this._content && this._content.frame !== value) {
this._content.frame = value;
this.updateGear(5);
}
}
/**
* Get/set color tint
* 获取/设置颜色着色
*/
public get color(): string {
return this._content?.color ?? '#FFFFFF';
}
public set color(value: string) {
if (this._content && this._content.color !== value) {
this._content.color = value;
this.updateGear(4);
}
}
/**
* Get/set fill method
* 获取/设置填充方法
*/
public get fillMethod(): EFillMethod {
return this._content?.fillMethod ?? EFillMethod.None;
}
public set fillMethod(value: EFillMethod) {
if (this._content) {
this._content.fillMethod = value;
}
}
/**
* Get/set fill origin
* 获取/设置填充起点
*/
public get fillOrigin(): number {
return this._content?.fillOrigin ?? 0;
}
public set fillOrigin(value: number) {
if (this._content) {
this._content.fillOrigin = value;
}
}
/**
* Get/set fill clockwise
* 获取/设置顺时针填充
*/
public get fillClockwise(): boolean {
return this._content?.fillClockwise ?? true;
}
public set fillClockwise(value: boolean) {
if (this._content) {
this._content.fillClockwise = value;
}
}
/**
* Get/set fill amount
* 获取/设置填充量
*/
public get fillAmount(): number {
return this._content?.fillAmount ?? 1;
}
public set fillAmount(value: number) {
if (this._content) {
this._content.fillAmount = value;
}
}
/**
* Get/set show error sign
* 获取/设置显示错误标志
*/
public get showErrorSign(): boolean {
return this._showErrorSign;
}
public set showErrorSign(value: boolean) {
this._showErrorSign = value;
}
/**
* Get internal content (MovieClip)
* 获取内部内容MovieClip
*/
public get content(): MovieClip {
return this._content;
}
/**
* Get component content (when loading component)
* 获取组件内容(当加载组件时)
*/
public get component(): GComponent | null {
return this._content2;
}
/**
* Load content based on URL
* 根据 URL 加载内容
*/
protected loadContent(): void {
this.clearContent();
if (!this._url) return;
if (this._url.startsWith('ui://')) {
this.loadFromPackage(this._url);
} else {
this.loadExternal();
}
}
/**
* Load content from package
* 从包加载内容
*/
protected loadFromPackage(itemURL: string): void {
this._contentItem = UIPackage.getItemByURL(itemURL);
if (this._contentItem) {
// Get branch and high resolution versions
const branchItem = this._contentItem.getBranch();
this.sourceWidth = branchItem.width;
this.sourceHeight = branchItem.height;
const hiResItem = branchItem.getHighResolution();
hiResItem.load();
if (this._autoSize) {
this.setSize(this.sourceWidth, this.sourceHeight);
}
if (hiResItem.type === EPackageItemType.Image) {
if (!hiResItem.texture) {
this.setErrorState();
} else {
this._content.texture = hiResItem.texture;
this._content.scale9Grid = hiResItem.scale9Grid
? new Rectangle(
hiResItem.scale9Grid.x,
hiResItem.scale9Grid.y,
hiResItem.scale9Grid.width,
hiResItem.scale9Grid.height
)
: null;
this._content.scaleByTile = hiResItem.scaleByTile || false;
this._content.tileGridIndice = hiResItem.tileGridIndice || 0;
this.sourceWidth = hiResItem.width;
this.sourceHeight = hiResItem.height;
this.updateLayout();
}
} else if (hiResItem.type === EPackageItemType.MovieClip) {
this.sourceWidth = hiResItem.width;
this.sourceHeight = hiResItem.height;
this._content.interval = hiResItem.interval || 0;
this._content.swing = hiResItem.swing || false;
this._content.repeatDelay = hiResItem.repeatDelay || 0;
this._content.frames = hiResItem.frames || [];
this.updateLayout();
} else if (hiResItem.type === EPackageItemType.Component) {
const obj = UIPackage.createObjectFromURL(itemURL);
if (!obj) {
this.setErrorState();
} else if (!(obj instanceof GComponent)) {
obj.dispose();
this.setErrorState();
} else {
this._content2 = obj;
if (this._displayObject && this._content2.displayObject) {
this._displayObject.addChild(this._content2.displayObject);
}
this.updateLayout();
}
} else {
this.setErrorState();
}
} else {
this.setErrorState();
}
}
/**
* Load external resource (to be overridden)
* 加载外部资源(可重写)
*/
protected loadExternal(): void {
// Default implementation: load image via fetch
this.loadExternalImage(this._url);
}
/**
* Load external image
* 加载外部图像
*/
protected async loadExternalImage(url: string): Promise<void> {
try {
const response = await fetch(url);
const blob = await response.blob();
const bitmap = await createImageBitmap(blob);
// Create texture ID from URL
this.onExternalLoadSuccess(url, bitmap.width, bitmap.height);
} catch (error) {
console.error(`Failed to load external image: ${url}`, error);
this.onExternalLoadFailed();
}
}
/**
* Free external resource
* 释放外部资源
*/
protected freeExternal(_texture: string | number | ISpriteTexture | null): void {
// Override in subclass if needed
}
/**
* Called when external resource loaded successfully
* 外部资源加载成功时调用
*/
protected onExternalLoadSuccess(textureId: string | number, width: number, height: number): void {
this._content.texture = textureId;
this._content.scale9Grid = null;
this._content.scaleByTile = false;
this.sourceWidth = width;
this.sourceHeight = height;
this.updateLayout();
}
/**
* Called when external resource failed to load
* 外部资源加载失败时调用
*/
protected onExternalLoadFailed(): void {
this.setErrorState();
}
/**
* Set error state and show error sign
* 设置错误状态并显示错误标志
*/
private setErrorState(): void {
if (!this._showErrorSign) return;
if (!this._errorSign) {
const errorSignUrl = getUIConfig('loaderErrorSign');
if (errorSignUrl) {
this._errorSign = GLoader._errorSignPool.getObject(errorSignUrl);
}
}
if (this._errorSign) {
this._errorSign.setSize(this.width, this.height);
if (this._displayObject && this._errorSign.displayObject) {
this._displayObject.addChild(this._errorSign.displayObject);
}
}
}
/**
* Clear error state
* 清除错误状态
*/
private clearErrorState(): void {
if (this._errorSign) {
if (this._displayObject && this._errorSign.displayObject) {
this._displayObject.removeChild(this._errorSign.displayObject);
}
GLoader._errorSignPool.returnObject(this._errorSign);
this._errorSign = null;
}
}
/**
* Update content layout
* 更新内容布局
*/
protected updateLayout(): void {
if (!this._content) return;
if (!this._content2 && !this._content.texture && !this._content.frames?.length) {
if (this._autoSize) {
this._updatingLayout = true;
this.setSize(50, 30);
this._updatingLayout = false;
}
return;
}
let cw = this.sourceWidth;
let ch = this.sourceHeight;
if (this._autoSize) {
this._updatingLayout = true;
if (cw === 0) cw = 50;
if (ch === 0) ch = 30;
this.setSize(cw, ch);
this._updatingLayout = false;
if (cw === this._width && ch === this._height) {
if (this._content2) {
this._content2.setXY(0, 0);
if (this._useResize) {
this._content2.setSize(cw, ch);
} else {
this._content2.setScale(1, 1);
}
} else {
this._content.width = cw;
this._content.height = ch;
this._content.x = 0;
this._content.y = 0;
}
return;
}
}
let sx = 1;
let sy = 1;
if (this._fill !== ELoaderFillType.None) {
sx = this.width / this.sourceWidth;
sy = this.height / this.sourceHeight;
if (sx !== 1 || sy !== 1) {
if (this._fill === ELoaderFillType.ScaleMatchHeight) {
sx = sy;
} else if (this._fill === ELoaderFillType.ScaleMatchWidth) {
sy = sx;
} else if (this._fill === ELoaderFillType.Scale) {
if (sx > sy) sx = sy;
else sy = sx;
} else if (this._fill === ELoaderFillType.ScaleNoBorder) {
if (sx > sy) sy = sx;
else sx = sy;
}
if (this._shrinkOnly) {
if (sx > 1) sx = 1;
if (sy > 1) sy = 1;
}
cw = this.sourceWidth * sx;
ch = this.sourceHeight * sy;
}
}
if (this._content2) {
if (this._useResize) {
this._content2.setSize(cw, ch);
} else {
this._content2.setScale(sx, sy);
}
} else {
this._content.width = cw;
this._content.height = ch;
}
// Calculate position based on alignment
let nx = 0;
let ny = 0;
if (this._align === EAlignType.Center) {
nx = Math.floor((this.width - cw) / 2);
} else if (this._align === EAlignType.Right) {
nx = this.width - cw;
}
if (this._valign === EVertAlignType.Middle) {
ny = Math.floor((this.height - ch) / 2);
} else if (this._valign === EVertAlignType.Bottom) {
ny = this.height - ch;
}
if (this._content2) {
this._content2.setXY(nx, ny);
} else {
this._content.x = nx;
this._content.y = ny;
}
}
/**
* Clear content
* 清除内容
*/
private clearContent(): void {
this.clearErrorState();
if (this._content) {
if (!this._contentItem && this._content.texture) {
this.freeExternal(this._content.texture);
}
this._content.texture = null;
this._content.frames = [];
}
if (this._content2) {
this._content2.dispose();
this._content2 = null;
}
this._contentItem = null;
}
protected handleSizeChanged(): void {
super.handleSizeChanged();
if (!this._updatingLayout) {
this.updateLayout();
}
}
public getProp(index: number): any {
switch (index) {
case EObjectPropID.Color:
return this.color;
case EObjectPropID.Playing:
return this.playing;
case EObjectPropID.Frame:
return this.frame;
case EObjectPropID.TimeScale:
return this._content?.timeScale ?? 1;
default:
return super.getProp(index);
}
}
public setProp(index: number, value: any): void {
switch (index) {
case EObjectPropID.Color:
this.color = value;
break;
case EObjectPropID.Playing:
this.playing = value;
break;
case EObjectPropID.Frame:
this.frame = value;
break;
case EObjectPropID.TimeScale:
if (this._content) {
this._content.timeScale = value;
}
break;
case EObjectPropID.DeltaTime:
if (this._content) {
this._content.advance(value);
}
break;
default:
super.setProp(index, value);
break;
}
}
public setup_beforeAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_beforeAdd(buffer, beginPos);
buffer.seek(beginPos, 5);
this._url = buffer.readS();
const alignValue = buffer.readByte();
this._align =
alignValue === 0 ? EAlignType.Left : alignValue === 1 ? EAlignType.Center : EAlignType.Right;
const valignValue = buffer.readByte();
this._valign =
valignValue === 0 ? EVertAlignType.Top : valignValue === 1 ? EVertAlignType.Middle : EVertAlignType.Bottom;
this._fill = buffer.readByte();
this._shrinkOnly = buffer.readBool();
this._autoSize = buffer.readBool();
this._showErrorSign = buffer.readBool();
const playingValue = buffer.readBool();
const frameValue = buffer.getInt32();
if (this._content) {
this._content.playing = playingValue;
this._content.frame = frameValue;
}
if (buffer.readBool()) {
this.color = buffer.readS();
}
const fillMethodValue = buffer.readByte();
if (this._content) {
this._content.fillMethod = fillMethodValue;
if (this._content.fillMethod !== EFillMethod.None) {
this._content.fillOrigin = buffer.readByte();
this._content.fillClockwise = buffer.readBool();
this._content.fillAmount = buffer.getFloat32();
}
} else if (fillMethodValue !== EFillMethod.None) {
// Skip bytes if _content not ready
buffer.readByte(); // fillOrigin
buffer.readBool(); // fillClockwise
buffer.getFloat32(); // fillAmount
}
if (buffer.version >= 7) {
this._useResize = buffer.readBool();
}
if (this._url) {
this.loadContent();
}
}
}

View File

@@ -0,0 +1,261 @@
import { GObject } from '../core/GObject';
import { MovieClip, type IFrame } from '../display/MovieClip';
import { EFlipType, EObjectPropID } from '../core/FieldTypes';
import type { ByteBuffer } from '../utils/ByteBuffer';
import type { PackageItem } from '../package/PackageItem';
/**
* GMovieClip
*
* Movie clip display object for FairyGUI animations.
*
* FairyGUI 动画显示对象
*/
export class GMovieClip extends GObject {
private _movieClip!: MovieClip;
private _flip: EFlipType = EFlipType.None;
private _contentItem: PackageItem | null = null;
constructor() {
super();
this.ensureMovieClip();
}
private ensureMovieClip(): void {
if (!this._movieClip) {
this.createDisplayObject();
}
}
/**
* Get the internal movie clip display object
* 获取内部动画显示对象
*/
public get movieClip(): MovieClip {
return this._movieClip;
}
/**
* Get/set playing state
* 获取/设置播放状态
*/
public get playing(): boolean {
return this._movieClip.playing;
}
public set playing(value: boolean) {
if (this._movieClip && this._movieClip.playing !== value) {
this._movieClip.playing = value;
this.updateGear(5);
}
}
/**
* Get/set current frame
* 获取/设置当前帧
*/
public get frame(): number {
return this._movieClip.frame;
}
public set frame(value: number) {
if (this._movieClip && this._movieClip.frame !== value) {
this._movieClip.frame = value;
this.updateGear(5);
}
}
/**
* Get/set color tint
* 获取/设置颜色着色
*/
public get color(): string {
return this._movieClip.color;
}
public set color(value: string) {
if (this._movieClip) {
this._movieClip.color = value;
this.updateGear(4);
}
}
/**
* Get/set flip type
* 获取/设置翻转类型
*/
public get flip(): EFlipType {
return this._flip;
}
public set flip(value: EFlipType) {
if (this._flip !== value) {
this._flip = value;
let sx = 1;
let sy = 1;
if (this._flip === EFlipType.Horizontal || this._flip === EFlipType.Both) {
sx = -1;
}
if (this._flip === EFlipType.Vertical || this._flip === EFlipType.Both) {
sy = -1;
}
this.setScale(sx, sy);
this.handleXYChanged();
}
}
/**
* Get/set time scale
* 获取/设置时间缩放
*/
public get timeScale(): number {
return this._movieClip.timeScale;
}
public set timeScale(value: number) {
if (this._movieClip) {
this._movieClip.timeScale = value;
}
}
/**
* Rewind to beginning
* 回到开始
*/
public rewind(): void {
this._movieClip.rewind();
}
/**
* Sync status with another movie clip
* 同步状态
*/
public syncStatus(anotherMc: GMovieClip): void {
this._movieClip.syncStatus(anotherMc._movieClip);
}
/**
* Advance by time
* 按时间前进
*/
public advance(time: number): void {
this._movieClip.advance(time);
}
/**
* Set play settings
* 设置播放设置
*/
public setPlaySettings(
start: number = 0,
end: number = -1,
times: number = 0,
endAt: number = -1,
endCallback?: () => void
): void {
this._movieClip.setPlaySettings(start, end, times, endAt, endCallback);
}
protected createDisplayObject(): void {
this._displayObject = this._movieClip = new MovieClip();
this._displayObject.gOwner = this;
}
/**
* Construct from package resource
* 从包资源构建
*/
public constructFromResource(): void {
if (!this.packageItem) return;
this.ensureMovieClip();
this._contentItem = this.packageItem;
this.sourceWidth = this._contentItem.width;
this.sourceHeight = this._contentItem.height;
this.initWidth = this.sourceWidth;
this.initHeight = this.sourceHeight;
// Load frames from package
if (this._contentItem.owner) {
this._contentItem.owner.getItemAsset(this._contentItem);
}
if (this._contentItem.frames) {
this._movieClip.interval = this._contentItem.interval;
this._movieClip.swing = this._contentItem.swing;
this._movieClip.repeatDelay = this._contentItem.repeatDelay;
this._movieClip.frames = this._contentItem.frames as IFrame[];
}
this.setSize(this.sourceWidth, this.sourceHeight);
}
protected handleXYChanged(): void {
super.handleXYChanged();
if (this._flip !== EFlipType.None) {
if (this.scaleX === -1 && this._movieClip) {
this._movieClip.x += this.width;
}
if (this.scaleY === -1 && this._movieClip) {
this._movieClip.y += this.height;
}
}
}
public getProp(index: number): any {
switch (index) {
case EObjectPropID.Color:
return this.color;
case EObjectPropID.Playing:
return this.playing;
case EObjectPropID.Frame:
return this.frame;
case EObjectPropID.TimeScale:
return this.timeScale;
default:
return super.getProp(index);
}
}
public setProp(index: number, value: any): void {
switch (index) {
case EObjectPropID.Color:
this.color = value;
break;
case EObjectPropID.Playing:
this.playing = value;
break;
case EObjectPropID.Frame:
this.frame = value;
break;
case EObjectPropID.TimeScale:
this.timeScale = value;
break;
default:
super.setProp(index, value);
break;
}
}
public setup_beforeAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_beforeAdd(buffer, beginPos);
buffer.seek(beginPos, 5);
if (buffer.readBool()) {
this.color = buffer.readS();
}
this.flip = buffer.readByte();
const frameValue = buffer.getInt32();
const playingValue = buffer.readBool();
if (this._movieClip) {
this._movieClip.frame = frameValue;
this._movieClip.playing = playingValue;
}
}
}

View File

@@ -0,0 +1,228 @@
import { GComponent } from '../core/GComponent';
import { GObject } from '../core/GObject';
import { GImage } from './GImage';
import { EProgressTitleType, EObjectPropID, EFillMethod } from '../core/FieldTypes';
import type { ByteBuffer } from '../utils/ByteBuffer';
/**
* GProgressBar
*
* Progress bar component.
*
* 进度条组件
*/
export class GProgressBar extends GComponent {
private _min: number = 0;
private _max: number = 100;
private _value: number = 50;
private _titleType: EProgressTitleType = EProgressTitleType.Percent;
private _reverse: boolean = false;
private _titleObject: GObject | null = null;
private _aniObject: GObject | null = null;
private _barObjectH: GObject | null = null;
private _barObjectV: GObject | null = null;
private _barMaxWidth: number = 0;
private _barMaxHeight: number = 0;
private _barMaxWidthDelta: number = 0;
private _barMaxHeightDelta: number = 0;
private _barStartX: number = 0;
private _barStartY: number = 0;
constructor() {
super();
}
/**
* Get/set title type
* 获取/设置标题类型
*/
public get titleType(): EProgressTitleType {
return this._titleType;
}
public set titleType(value: EProgressTitleType) {
if (this._titleType !== value) {
this._titleType = value;
this.update(this._value);
}
}
/**
* Get/set minimum value
* 获取/设置最小值
*/
public get min(): number {
return this._min;
}
public set min(value: number) {
if (this._min !== value) {
this._min = value;
this.update(this._value);
}
}
/**
* Get/set maximum value
* 获取/设置最大值
*/
public get max(): number {
return this._max;
}
public set max(value: number) {
if (this._max !== value) {
this._max = value;
this.update(this._value);
}
}
/**
* Get/set current value
* 获取/设置当前值
*/
public get value(): number {
return this._value;
}
public set value(value: number) {
if (this._value !== value) {
this._value = value;
this.update(value);
}
}
/**
* Update progress bar display
* 更新进度条显示
*/
public update(newValue: number): void {
let percent = this.clamp01((newValue - this._min) / (this._max - this._min));
if (this._titleObject) {
switch (this._titleType) {
case EProgressTitleType.Percent:
this._titleObject.text = Math.floor(percent * 100) + '%';
break;
case EProgressTitleType.ValueAndMax:
this._titleObject.text =
Math.floor(newValue) + '/' + Math.floor(this._max);
break;
case EProgressTitleType.Value:
this._titleObject.text = '' + Math.floor(newValue);
break;
case EProgressTitleType.Max:
this._titleObject.text = '' + Math.floor(this._max);
break;
}
}
const fullWidth = this.width - this._barMaxWidthDelta;
const fullHeight = this.height - this._barMaxHeightDelta;
if (!this._reverse) {
if (this._barObjectH) {
if (!this.setFillAmount(this._barObjectH, percent)) {
this._barObjectH.width = Math.round(fullWidth * percent);
}
}
if (this._barObjectV) {
if (!this.setFillAmount(this._barObjectV, percent)) {
this._barObjectV.height = Math.round(fullHeight * percent);
}
}
} else {
if (this._barObjectH) {
if (!this.setFillAmount(this._barObjectH, 1 - percent)) {
this._barObjectH.width = Math.round(fullWidth * percent);
this._barObjectH.x = this._barStartX + (fullWidth - this._barObjectH.width);
}
}
if (this._barObjectV) {
if (!this.setFillAmount(this._barObjectV, 1 - percent)) {
this._barObjectV.height = Math.round(fullHeight * percent);
this._barObjectV.y =
this._barStartY + (fullHeight - this._barObjectV.height);
}
}
}
if (this._aniObject) {
this._aniObject.setProp(EObjectPropID.Frame, Math.floor(percent * 100));
}
}
private setFillAmount(bar: GObject, percent: number): boolean {
if (bar instanceof GImage && bar.fillMethod !== EFillMethod.None) {
bar.fillAmount = percent;
return true;
}
return false;
}
private clamp01(value: number): number {
if (value < 0) return 0;
if (value > 1) return 1;
return value;
}
protected constructExtension(buffer: ByteBuffer): void {
buffer.seek(0, 6);
this._titleType = buffer.readByte();
this._reverse = buffer.readBool();
this._titleObject = this.getChild('title');
this._barObjectH = this.getChild('bar');
this._barObjectV = this.getChild('bar_v');
this._aniObject = this.getChild('ani');
if (this._barObjectH) {
this._barMaxWidth = this._barObjectH.width;
this._barMaxWidthDelta = this.width - this._barMaxWidth;
this._barStartX = this._barObjectH.x;
}
if (this._barObjectV) {
this._barMaxHeight = this._barObjectV.height;
this._barMaxHeightDelta = this.height - this._barMaxHeight;
this._barStartY = this._barObjectV.y;
}
}
protected handleSizeChanged(): void {
super.handleSizeChanged();
if (this._barObjectH) {
this._barMaxWidth = this.width - this._barMaxWidthDelta;
}
if (this._barObjectV) {
this._barMaxHeight = this.height - this._barMaxHeightDelta;
}
if (!this._underConstruct) {
this.update(this._value);
}
}
public setup_afterAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_afterAdd(buffer, beginPos);
if (!buffer.seek(beginPos, 6)) {
this.update(this._value);
return;
}
if (buffer.readByte() !== this.packageItem?.objectType) {
this.update(this._value);
return;
}
this._value = buffer.getInt32();
this._max = buffer.getInt32();
if (buffer.version >= 2) {
this._min = buffer.getInt32();
}
this.update(this._value);
}
}

View File

@@ -0,0 +1,332 @@
import { GComponent } from '../core/GComponent';
import { GObject } from '../core/GObject';
import { FGUIEvents } from '../events/Events';
import { EProgressTitleType } from '../core/FieldTypes';
import { Point } from '../utils/MathTypes';
import type { ByteBuffer } from '../utils/ByteBuffer';
/**
* GSlider
*
* Slider component with draggable grip.
*
* 滑动条组件,支持拖动手柄
*/
export class GSlider extends GComponent {
private _min: number = 0;
private _max: number = 100;
private _value: number = 50;
private _titleType: EProgressTitleType = EProgressTitleType.Percent;
private _reverse: boolean = false;
private _wholeNumbers: boolean = false;
private _titleObject: GObject | null = null;
private _barObjectH: GObject | null = null;
private _barObjectV: GObject | null = null;
private _barMaxWidth: number = 0;
private _barMaxHeight: number = 0;
private _barMaxWidthDelta: number = 0;
private _barMaxHeightDelta: number = 0;
private _gripObject: GObject | null = null;
private _clickPos: Point = new Point();
private _clickPercent: number = 0;
private _barStartX: number = 0;
private _barStartY: number = 0;
/** Allow click on bar to change value | 允许点击条改变值 */
public changeOnClick: boolean = true;
/** Allow dragging | 允许拖动 */
public canDrag: boolean = true;
constructor() {
super();
}
/**
* Get/set title type
* 获取/设置标题类型
*/
public get titleType(): EProgressTitleType {
return this._titleType;
}
public set titleType(value: EProgressTitleType) {
this._titleType = value;
}
/**
* Get/set whole numbers mode
* 获取/设置整数模式
*/
public get wholeNumbers(): boolean {
return this._wholeNumbers;
}
public set wholeNumbers(value: boolean) {
if (this._wholeNumbers !== value) {
this._wholeNumbers = value;
this.update();
}
}
/**
* Get/set minimum value
* 获取/设置最小值
*/
public get min(): number {
return this._min;
}
public set min(value: number) {
if (this._min !== value) {
this._min = value;
this.update();
}
}
/**
* Get/set maximum value
* 获取/设置最大值
*/
public get max(): number {
return this._max;
}
public set max(value: number) {
if (this._max !== value) {
this._max = value;
this.update();
}
}
/**
* Get/set current value
* 获取/设置当前值
*/
public get value(): number {
return this._value;
}
public set value(value: number) {
if (this._value !== value) {
this._value = value;
this.update();
}
}
/**
* Update slider display
* 更新滑动条显示
*/
public update(): void {
this.updateWithPercent(
(this._value - this._min) / (this._max - this._min),
false
);
}
private updateWithPercent(percent: number, bEmitEvent: boolean): void {
percent = this.clamp01(percent);
if (bEmitEvent) {
let newValue = this.clamp(
this._min + (this._max - this._min) * percent,
this._min,
this._max
);
if (this._wholeNumbers) {
newValue = Math.round(newValue);
percent = this.clamp01((newValue - this._min) / (this._max - this._min));
}
if (newValue !== this._value) {
this._value = newValue;
this.emit(FGUIEvents.STATE_CHANGED);
}
}
if (this._titleObject) {
switch (this._titleType) {
case EProgressTitleType.Percent:
this._titleObject.text = Math.floor(percent * 100) + '%';
break;
case EProgressTitleType.ValueAndMax:
this._titleObject.text = this._value + '/' + this._max;
break;
case EProgressTitleType.Value:
this._titleObject.text = '' + this._value;
break;
case EProgressTitleType.Max:
this._titleObject.text = '' + this._max;
break;
}
}
const fullWidth = this.width - this._barMaxWidthDelta;
const fullHeight = this.height - this._barMaxHeightDelta;
if (!this._reverse) {
if (this._barObjectH) {
this._barObjectH.width = Math.round(fullWidth * percent);
}
if (this._barObjectV) {
this._barObjectV.height = Math.round(fullHeight * percent);
}
} else {
if (this._barObjectH) {
this._barObjectH.width = Math.round(fullWidth * percent);
this._barObjectH.x = this._barStartX + (fullWidth - this._barObjectH.width);
}
if (this._barObjectV) {
this._barObjectV.height = Math.round(fullHeight * percent);
this._barObjectV.y =
this._barStartY + (fullHeight - this._barObjectV.height);
}
}
}
private clamp01(value: number): number {
if (value < 0) return 0;
if (value > 1) return 1;
return value;
}
private clamp(value: number, min: number, max: number): number {
if (value < min) return min;
if (value > max) return max;
return value;
}
protected constructExtension(buffer: ByteBuffer): void {
buffer.seek(0, 6);
this._titleType = buffer.readByte();
this._reverse = buffer.readBool();
if (buffer.version >= 2) {
this._wholeNumbers = buffer.readBool();
this.changeOnClick = buffer.readBool();
}
this._titleObject = this.getChild('title');
this._barObjectH = this.getChild('bar');
this._barObjectV = this.getChild('bar_v');
this._gripObject = this.getChild('grip');
if (this._barObjectH) {
this._barMaxWidth = this._barObjectH.width;
this._barMaxWidthDelta = this.width - this._barMaxWidth;
this._barStartX = this._barObjectH.x;
}
if (this._barObjectV) {
this._barMaxHeight = this._barObjectV.height;
this._barMaxHeightDelta = this.height - this._barMaxHeight;
this._barStartY = this._barObjectV.y;
}
if (this._gripObject) {
this._gripObject.on(FGUIEvents.TOUCH_BEGIN, this.handleGripTouchBegin, this);
}
this.on(FGUIEvents.TOUCH_BEGIN, this.handleBarTouchBegin, this);
}
protected handleSizeChanged(): void {
super.handleSizeChanged();
if (this._barObjectH) {
this._barMaxWidth = this.width - this._barMaxWidthDelta;
}
if (this._barObjectV) {
this._barMaxHeight = this.height - this._barMaxHeightDelta;
}
if (!this._underConstruct) {
this.update();
}
}
public setup_afterAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_afterAdd(buffer, beginPos);
if (!buffer.seek(beginPos, 6)) {
this.update();
return;
}
if (buffer.readByte() !== this.packageItem?.objectType) {
this.update();
return;
}
this._value = buffer.getInt32();
this._max = buffer.getInt32();
if (buffer.version >= 2) {
this._min = buffer.getInt32();
}
this.update();
}
private handleGripTouchBegin(evt: any): void {
this.canDrag = true;
if (evt.stopPropagation) {
evt.stopPropagation();
}
this._clickPos = this.globalToLocal(evt.stageX || 0, evt.stageY || 0);
this._clickPercent = this.clamp01(
(this._value - this._min) / (this._max - this._min)
);
this.root?.on(FGUIEvents.TOUCH_MOVE, this.handleGripTouchMove, this);
this.root?.on(FGUIEvents.TOUCH_END, this.handleGripTouchEnd, this);
}
private handleGripTouchMove(evt: any): void {
if (!this.canDrag) {
return;
}
const pt = this.globalToLocal(evt.stageX || 0, evt.stageY || 0);
let deltaX = pt.x - this._clickPos.x;
let deltaY = pt.y - this._clickPos.y;
if (this._reverse) {
deltaX = -deltaX;
deltaY = -deltaY;
}
let percent: number;
if (this._barObjectH) {
percent = this._clickPercent + deltaX / this._barMaxWidth;
} else {
percent = this._clickPercent + deltaY / this._barMaxHeight;
}
this.updateWithPercent(percent, true);
}
private handleGripTouchEnd(): void {
this.root?.off(FGUIEvents.TOUCH_MOVE, this.handleGripTouchMove, this);
this.root?.off(FGUIEvents.TOUCH_END, this.handleGripTouchEnd, this);
}
private handleBarTouchBegin(evt: any): void {
if (!this.changeOnClick || !this._gripObject) {
return;
}
const pt = this._gripObject.globalToLocal(evt.stageX || 0, evt.stageY || 0);
let percent = this.clamp01((this._value - this._min) / (this._max - this._min));
let delta: number = 0;
if (this._barObjectH) {
delta = pt.x / this._barMaxWidth;
}
if (this._barObjectV) {
delta = pt.y / this._barMaxHeight;
}
if (this._reverse) {
percent -= delta;
} else {
percent += delta;
}
this.updateWithPercent(percent, true);
}
}

View File

@@ -0,0 +1,470 @@
import { GObject } from '../core/GObject';
import { TextField } from '../display/TextField';
import {
EAutoSizeType,
EAlignType,
EVertAlignType,
EObjectPropID
} from '../core/FieldTypes';
import type { ByteBuffer } from '../utils/ByteBuffer';
/**
* GTextField
*
* Text field display object for FairyGUI.
*
* FairyGUI 文本字段显示对象
*/
export class GTextField extends GObject {
protected _textField!: TextField;
protected _text: string = '';
protected _autoSize: EAutoSizeType = EAutoSizeType.Both;
protected _widthAutoSize: boolean = true;
protected _heightAutoSize: boolean = true;
protected _color: string = '#000000';
protected _singleLine: boolean = false;
constructor() {
super();
// Ensure _textField is initialized - super() calls createDisplayObject() but
// class field initializers run after super(), which may cause issues
this.ensureTextField();
}
private ensureTextField(): void {
if (!this._textField) {
this.createDisplayObject();
}
}
protected createDisplayObject(): void {
this._displayObject = this._textField = new TextField();
this._textField.touchable = false;
this._displayObject.gOwner = this;
}
/**
* Get the internal text field display object
* 获取内部文本字段显示对象
*/
public get textField(): TextField {
return this._textField;
}
/**
* Get/set text content
* 获取/设置文本内容
*/
public get text(): string {
return this._text;
}
public set text(value: string) {
this._text = value;
if (this._textField) {
this._textField.text = value;
this.updateSize();
}
}
/**
* Get/set font
* 获取/设置字体
*/
public get font(): string {
return this._textField.font;
}
public set font(value: string) {
if (this._textField) {
this._textField.font = value;
}
}
/**
* Get/set font size
* 获取/设置字体大小
*/
public get fontSize(): number {
return this._textField.fontSize;
}
public set fontSize(value: number) {
if (this._textField) {
this._textField.fontSize = value;
}
}
/**
* Get/set text color
* 获取/设置文本颜色
*/
public get color(): string {
return this._color;
}
public set color(value: string) {
if (this._color !== value) {
this._color = value;
this.updateGear(4);
if (this.grayed) {
this._textField.color = '#AAAAAA';
} else {
this._textField.color = this._color;
}
}
}
/**
* Get/set horizontal alignment
* 获取/设置水平对齐
*/
public get align(): EAlignType {
return this._textField.align;
}
public set align(value: EAlignType) {
if (this._textField) {
this._textField.align = value;
}
}
/**
* Get/set vertical alignment
* 获取/设置垂直对齐
*/
public get valign(): EVertAlignType {
return this._textField.valign;
}
public set valign(value: EVertAlignType) {
if (this._textField) {
this._textField.valign = value;
}
}
/**
* Get/set leading (line spacing)
* 获取/设置行间距
*/
public get leading(): number {
return this._textField.leading;
}
public set leading(value: number) {
if (this._textField) {
this._textField.leading = value;
}
}
/**
* Get/set letter spacing
* 获取/设置字间距
*/
public get letterSpacing(): number {
return this._textField.letterSpacing;
}
public set letterSpacing(value: number) {
if (this._textField) {
this._textField.letterSpacing = value;
}
}
/**
* Get/set bold
* 获取/设置粗体
*/
public get bold(): boolean {
return this._textField.bold;
}
public set bold(value: boolean) {
if (this._textField) {
this._textField.bold = value;
}
}
/**
* Get/set italic
* 获取/设置斜体
*/
public get italic(): boolean {
return this._textField.italic;
}
public set italic(value: boolean) {
if (this._textField) {
this._textField.italic = value;
}
}
/**
* Get/set underline
* 获取/设置下划线
*/
public get underline(): boolean {
return this._textField.underline;
}
public set underline(value: boolean) {
if (this._textField) {
this._textField.underline = value;
}
}
/**
* Get/set single line mode
* 获取/设置单行模式
*/
public get singleLine(): boolean {
return this._singleLine;
}
public set singleLine(value: boolean) {
this._singleLine = value;
if (this._textField) {
this._textField.singleLine = value;
this._textField.wordWrap = !this._widthAutoSize && !this._singleLine;
}
}
/**
* Get/set stroke width
* 获取/设置描边宽度
*/
public get stroke(): number {
return this._textField.stroke;
}
public set stroke(value: number) {
if (this._textField) {
this._textField.stroke = value;
}
}
/**
* Get/set stroke color
* 获取/设置描边颜色
*/
public get strokeColor(): string {
return this._textField.strokeColor;
}
public set strokeColor(value: string) {
if (this._textField && this._textField.strokeColor !== value) {
this._textField.strokeColor = value;
this.updateGear(4);
}
}
/**
* Get/set UBB enabled
* 获取/设置 UBB 标签启用
*/
public get ubbEnabled(): boolean {
return this._textField.ubbEnabled;
}
public set ubbEnabled(value: boolean) {
if (this._textField) {
this._textField.ubbEnabled = value;
}
}
/**
* Get/set auto size type
* 获取/设置自动尺寸类型
*/
public get autoSize(): EAutoSizeType {
return this._autoSize;
}
public set autoSize(value: EAutoSizeType) {
if (this._autoSize !== value) {
this._autoSize = value;
this._widthAutoSize = this._autoSize === EAutoSizeType.Both;
this._heightAutoSize =
this._autoSize === EAutoSizeType.Both ||
this._autoSize === EAutoSizeType.Height;
this.updateAutoSize();
}
}
protected updateAutoSize(): void {
if (!this._textField) return;
this._textField.wordWrap = !this._widthAutoSize && !this._singleLine;
this._textField.autoSize = this._autoSize;
if (!this._underConstruct) {
if (!this._heightAutoSize) {
this._textField.width = this._width;
this._textField.height = this._height;
} else if (!this._widthAutoSize) {
this._textField.width = this._width;
}
}
}
/**
* Get text width
* 获取文本宽度
*/
public get textWidth(): number {
return this._textField.textWidth;
}
/**
* Get/set template variables
* 获取/设置模板变量
*/
public get templateVars(): Record<string, string> | null {
return this._textField.templateVars;
}
public set templateVars(value: Record<string, string> | null) {
if (this._textField) {
this._textField.templateVars = value;
}
}
/**
* Set a template variable
* 设置模板变量
*/
public setVar(name: string, value: string): GTextField {
if (this._textField) {
this._textField.setVar(name, value);
}
return this;
}
/**
* Flush template variables
* 刷新模板变量
*/
public flushVars(): void {
// Auto flush, nothing needed
}
public ensureSizeCorrect(): void {
// Force layout if needed
}
private updateSize(): void {
if (!this._textField) return;
if (this._widthAutoSize) {
this.setSize(this._textField.textWidth, this._textField.textHeight);
} else if (this._heightAutoSize) {
this.height = this._textField.textHeight;
}
}
protected handleSizeChanged(): void {
super.handleSizeChanged();
if (this._textField) {
this._textField.width = this._width;
this._textField.height = this._height;
}
}
protected handleGrayedChanged(): void {
super.handleGrayedChanged();
if (this._textField) {
if (this.grayed) {
this._textField.color = '#AAAAAA';
} else {
this._textField.color = this._color;
}
}
}
public getProp(index: number): any {
switch (index) {
case EObjectPropID.Color:
return this.color;
case EObjectPropID.OutlineColor:
return this.strokeColor;
case EObjectPropID.FontSize:
return this.fontSize;
default:
return super.getProp(index);
}
}
public setProp(index: number, value: any): void {
switch (index) {
case EObjectPropID.Color:
this.color = value;
break;
case EObjectPropID.OutlineColor:
this.strokeColor = value;
break;
case EObjectPropID.FontSize:
this.fontSize = value;
break;
default:
super.setProp(index, value);
break;
}
}
public setup_beforeAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_beforeAdd(buffer, beginPos);
buffer.seek(beginPos, 5);
this.font = buffer.readS();
this.fontSize = buffer.getInt16();
this.color = buffer.readColorS();
const alignValue = buffer.readByte();
this.align =
alignValue === 0
? EAlignType.Left
: alignValue === 1
? EAlignType.Center
: EAlignType.Right;
const valignValue = buffer.readByte();
this.valign =
valignValue === 0
? EVertAlignType.Top
: valignValue === 1
? EVertAlignType.Middle
: EVertAlignType.Bottom;
this.leading = buffer.getInt16();
this.letterSpacing = buffer.getInt16();
this.ubbEnabled = buffer.readBool();
this.autoSize = buffer.readByte();
this.underline = buffer.readBool();
this.italic = buffer.readBool();
this.bold = buffer.readBool();
this.singleLine = buffer.readBool();
if (buffer.readBool()) {
this.strokeColor = buffer.readColorS();
this.stroke = buffer.getFloat32() + 1;
}
if (buffer.readBool()) {
// Shadow - skip for now
buffer.skip(12);
}
if (buffer.readBool()) {
this._textField.templateVars = {};
}
}
public setup_afterAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_afterAdd(buffer, beginPos);
buffer.seek(beginPos, 6);
const str = buffer.readS();
if (str) {
this.text = str;
}
}
}

View File

@@ -0,0 +1,235 @@
import { GTextField } from './GTextField';
import { InputTextField } from '../display/InputTextField';
import { FGUIEvents } from '../events/Events';
import type { ByteBuffer } from '../utils/ByteBuffer';
/**
* Keyboard type constants
* 键盘类型常量
*/
export const enum EKeyboardType {
Default = 'text',
Number = 'number',
Url = 'url',
Email = 'email',
Tel = 'tel',
Password = 'password'
}
/**
* GTextInput
*
* Editable text input component.
*
* 可编辑的文本输入组件
*
* Features:
* - Text input with IME support
* - Password mode
* - Character restriction
* - Max length
* - Placeholder text
*/
export class GTextInput extends GTextField {
protected declare _displayObject: InputTextField;
constructor() {
super();
}
protected createDisplayObject(): void {
const inputField = new InputTextField();
// Set both _displayObject and _textField since parent class uses _textField for color etc.
this._displayObject = inputField;
this._textField = inputField;
this._displayObject.gOwner = this;
// Forward events
inputField.on('input', () => {
this.emit(FGUIEvents.TEXT_CHANGED);
});
inputField.on('submit', () => {
this.emit(FGUIEvents.TEXT_SUBMIT);
});
}
/**
* Get native input element
* 获取原生输入元素
*/
public get nativeInput(): InputTextField {
return this._displayObject;
}
/**
* Get/set password mode
* 获取/设置密码模式
*/
public get password(): boolean {
return this._displayObject.password;
}
public set password(value: boolean) {
if (this._displayObject) {
this._displayObject.password = value;
}
}
/**
* Get/set keyboard type
* 获取/设置键盘类型
*/
public get keyboardType(): string {
return this._displayObject.keyboardType;
}
public set keyboardType(value: string) {
if (this._displayObject) {
this._displayObject.keyboardType = value;
}
}
/**
* Get/set editable state
* 获取/设置可编辑状态
*/
public get editable(): boolean {
return this._displayObject.editable;
}
public set editable(value: boolean) {
if (this._displayObject) {
this._displayObject.editable = value;
}
}
/**
* Get/set max length
* 获取/设置最大长度
*/
public get maxLength(): number {
return this._displayObject.maxLength;
}
public set maxLength(value: number) {
if (this._displayObject) {
this._displayObject.maxLength = value;
}
}
/**
* Get/set placeholder text
* 获取/设置占位符文本
*/
public get promptText(): string {
return this._displayObject.promptText;
}
public set promptText(value: string) {
if (this._displayObject) {
this._displayObject.promptText = value;
}
}
/**
* Get/set placeholder color
* 获取/设置占位符颜色
*/
public get promptColor(): string {
return this._displayObject.promptColor;
}
public set promptColor(value: string) {
if (this._displayObject) {
this._displayObject.promptColor = value;
}
}
/**
* Get/set character restriction pattern
* 获取/设置字符限制模式
*/
public get restrict(): string {
return this._displayObject.restrict;
}
public set restrict(value: string) {
if (this._displayObject) {
this._displayObject.restrict = value;
}
}
/**
* Get/set single line mode
* 获取/设置单行模式
*/
public get singleLine(): boolean {
return this._singleLine;
}
public set singleLine(value: boolean) {
this._singleLine = value;
if (this._displayObject) {
this._displayObject.multiline = !value;
}
}
/**
* Request focus
* 请求焦点
*/
public requestFocus(): void {
this._displayObject.focus();
super.requestFocus();
}
/**
* Clear focus
* 清除焦点
*/
public clearFocus(): void {
this._displayObject.blur();
}
/**
* Select all text
* 选择所有文本
*/
public selectAll(): void {
this._displayObject.selectAll();
}
public setup_beforeAdd(buffer: ByteBuffer, beginPos: number): void {
super.setup_beforeAdd(buffer, beginPos);
buffer.seek(beginPos, 4);
let str = buffer.readS();
if (str) {
this.promptText = str;
}
str = buffer.readS();
if (str) {
this.restrict = str;
}
const iv = buffer.getInt32();
if (iv !== 0) {
this.maxLength = iv;
}
const keyboardTypeValue = buffer.getInt32();
if (keyboardTypeValue !== 0) {
if (keyboardTypeValue === 4) {
this.keyboardType = EKeyboardType.Number;
} else if (keyboardTypeValue === 3) {
this.keyboardType = EKeyboardType.Url;
}
}
if (buffer.readBool()) {
this.password = true;
}
}
}

View File

@@ -0,0 +1,325 @@
import { GComponent } from '../core/GComponent';
import { GObject } from '../core/GObject';
import { GRoot } from '../core/GRoot';
import { Controller } from '../core/Controller';
import { getUIConfig } from '../core/UIConfig';
import { UIPackage } from '../package/UIPackage';
import { ERelationType } from '../core/FieldTypes';
import { FGUIEvents } from '../events/Events';
import { Timer } from '../core/Timer';
import { GList } from './GList';
import { GButton } from './GButton';
import type { SimpleHandler } from '../display/MovieClip';
/**
* PopupMenu
*
* Context menu component with item management.
*
* 上下文菜单组件,支持菜单项管理
*
* Features:
* - Add/remove menu items
* - Checkable items
* - Separators
* - Grayed out items
*
* @example
* ```typescript
* const menu = new PopupMenu();
* menu.addItem('Open', () => console.log('Open clicked'));
* menu.addItem('Save', () => console.log('Save clicked'));
* menu.addSeperator();
* menu.addItem('Exit', () => console.log('Exit clicked'));
* menu.show(targetButton);
* ```
*/
export class PopupMenu {
protected _contentPane: GComponent;
protected _list: GList;
constructor(resourceURL?: string) {
if (!resourceURL) {
resourceURL = getUIConfig('popupMenu');
if (!resourceURL) {
throw new Error('UIConfig.popupMenu not defined');
}
}
const obj = UIPackage.createObjectFromURL(resourceURL);
if (!obj || !(obj instanceof GComponent)) {
throw new Error(`Failed to create popup menu from: ${resourceURL}`);
}
this._contentPane = obj;
this._contentPane.on(FGUIEvents.DISPLAY, this.onAddedToStage, this);
const list = this._contentPane.getChild('list');
if (!list || !(list instanceof GList)) {
throw new Error('PopupMenu content pane must have a child named "list" of type GList');
}
this._list = list;
this._list.removeChildrenToPool();
this._list.relations.add(this._contentPane, ERelationType.Width);
this._list.relations.remove(this._contentPane, ERelationType.Height);
this._contentPane.relations.add(this._list, ERelationType.Height);
this._list.on(FGUIEvents.CLICK_ITEM, this.onClickItem, this);
}
/**
* Dispose the menu
* 销毁菜单
*/
public dispose(): void {
this._contentPane.dispose();
}
/**
* Add a menu item
* 添加菜单项
*/
public addItem(caption: string, handler?: SimpleHandler): GButton {
const item = this._list.addItemFromPool();
if (!item || !(item instanceof GButton)) {
throw new Error('Failed to create menu item');
}
item.title = caption;
item.data = handler;
item.grayed = false;
const c = item.getController('checked');
if (c) {
c.selectedIndex = 0;
}
return item;
}
/**
* Add a menu item at specified index
* 在指定索引处添加菜单项
*/
public addItemAt(caption: string, index: number, handler?: SimpleHandler): GButton {
const item = this._list.getFromPool();
if (!item || !(item instanceof GButton)) {
throw new Error('Failed to create menu item');
}
this._list.addChildAt(item, index);
item.title = caption;
item.data = handler;
item.grayed = false;
const c = item.getController('checked');
if (c) {
c.selectedIndex = 0;
}
return item;
}
/**
* Add a separator
* 添加分隔符
*/
public addSeperator(): void {
const seperatorUrl = getUIConfig('popupMenuSeperator');
if (!seperatorUrl) {
throw new Error('UIConfig.popupMenuSeperator not defined');
}
this._list.addItemFromPool(seperatorUrl);
}
/**
* Get item name at index
* 获取指定索引处的菜单项名称
*/
public getItemName(index: number): string {
const item = this._list.getChildAt(index);
return item ? item.name : '';
}
/**
* Set item text by name
* 通过名称设置菜单项文本
*/
public setItemText(name: string, caption: string): void {
const item = this._list.getChild(name);
if (item && item instanceof GButton) {
item.title = caption;
}
}
/**
* Set item visibility by name
* 通过名称设置菜单项可见性
*/
public setItemVisible(name: string, bVisible: boolean): void {
const item = this._list.getChild(name);
if (item && item.visible !== bVisible) {
item.visible = bVisible;
this._list.setBoundsChangedFlag();
}
}
/**
* Set item grayed state by name
* 通过名称设置菜单项灰色状态
*/
public setItemGrayed(name: string, bGrayed: boolean): void {
const item = this._list.getChild(name);
if (item && item instanceof GButton) {
item.grayed = bGrayed;
}
}
/**
* Set item checkable state by name
* 通过名称设置菜单项可选中状态
*/
public setItemCheckable(name: string, bCheckable: boolean): void {
const item = this._list.getChild(name);
if (item && item instanceof GButton) {
const c = item.getController('checked');
if (c) {
if (bCheckable) {
if (c.selectedIndex === 0) {
c.selectedIndex = 1;
}
} else {
c.selectedIndex = 0;
}
}
}
}
/**
* Set item checked state by name
* 通过名称设置菜单项选中状态
*/
public setItemChecked(name: string, bChecked: boolean): void {
const item = this._list.getChild(name);
if (item && item instanceof GButton) {
const c = item.getController('checked');
if (c) {
c.selectedIndex = bChecked ? 2 : 1;
}
}
}
/**
* Check if item is checked by name
* 通过名称检查菜单项是否选中
*/
public isItemChecked(name: string): boolean {
const item = this._list.getChild(name);
if (item && item instanceof GButton) {
const c = item.getController('checked');
if (c) {
return c.selectedIndex === 2;
}
}
return false;
}
/**
* Remove item by name
* 通过名称移除菜单项
*/
public removeItem(name: string): boolean {
const item = this._list.getChild(name);
if (item) {
const index = this._list.getChildIndex(item);
this._list.removeChildToPoolAt(index);
return true;
}
return false;
}
/**
* Clear all items
* 清除所有菜单项
*/
public clearItems(): void {
this._list.removeChildrenToPool();
}
/**
* Get item count
* 获取菜单项数量
*/
public get itemCount(): number {
return this._list.numChildren;
}
/**
* Get content pane
* 获取内容面板
*/
public get contentPane(): GComponent {
return this._contentPane;
}
/**
* Get list component
* 获取列表组件
*/
public get list(): GList {
return this._list;
}
/**
* Show menu
* 显示菜单
*/
public show(target?: GObject, dir?: number): void {
const r = target?.root ?? GRoot.inst;
const popupTarget = target instanceof GRoot ? undefined : target;
r.showPopup(this._contentPane, popupTarget, dir);
}
private onClickItem(itemObject: GObject): void {
Timer.inst.callLater(this, () => this.handleItemClick(itemObject));
}
private handleItemClick(itemObject: GObject): void {
if (!(itemObject instanceof GButton)) {
return;
}
if (itemObject.grayed) {
this._list.selectedIndex = -1;
return;
}
const c = itemObject.getController('checked');
if (c && c.selectedIndex !== 0) {
if (c.selectedIndex === 1) {
c.selectedIndex = 2;
} else {
c.selectedIndex = 1;
}
}
const r = this._contentPane.parent as GRoot | null;
if (r) {
r.hidePopup(this._contentPane);
}
const handler = itemObject.data as SimpleHandler | null;
if (handler) {
if (typeof handler === 'function') {
handler();
} else if (typeof handler.run === 'function') {
handler.run();
}
}
}
private onAddedToStage(): void {
this._list.selectedIndex = -1;
this._list.resizeToFit(100000, 10);
}
}

View File

@@ -0,0 +1,522 @@
import { GComponent } from '../core/GComponent';
import { GObject } from '../core/GObject';
import { GRoot } from '../core/GRoot';
import { GGraph } from './GGraph';
import { getUIConfig } from '../core/UIConfig';
import { UIPackage } from '../package/UIPackage';
import { ERelationType } from '../core/FieldTypes';
import { FGUIEvents } from '../events/Events';
import { Point } from '../utils/MathTypes';
/**
* IUISource
*
* Interface for dynamic UI loading sources
* 动态 UI 加载源接口
*/
export interface IUISource {
/** Source file name | 源文件名 */
fileName: string;
/** Whether the source is loaded | 是否已加载 */
loaded: boolean;
/**
* Load the source
* 加载源
*/
load(callback: () => void, thisObj: any): void;
}
/**
* Window
*
* Base class for popup windows with modal support.
*
* 弹窗基类,支持模态窗口
*
* Features:
* - Content pane management
* - Modal wait indicator
* - Draggable title bar
* - Close button binding
* - Bring to front on click
*
* @example
* ```typescript
* class MyWindow extends Window {
* constructor() {
* super();
* this.contentPane = UIPackage.createObject('pkg', 'MyWindowContent') as GComponent;
* }
*
* protected onInit(): void {
* // Initialize window
* }
*
* protected onShown(): void {
* // Window is shown
* }
*
* protected onHide(): void {
* // Window is hidden
* }
* }
*
* const win = new MyWindow();
* win.show();
* ```
*/
export class Window extends GComponent {
/** Bring window to front when clicked | 点击时将窗口置顶 */
public bringToFrontOnClick: boolean;
protected _requestingCmd: number = 0;
private _contentPane: GComponent | null = null;
private _modalWaitPane: GObject | null = null;
private _closeButton: GObject | null = null;
private _dragArea: GObject | null = null;
private _contentArea: GObject | null = null;
private _frame: GComponent | null = null;
private _modal: boolean = false;
private _uiSources: IUISource[] = [];
private _inited: boolean = false;
private _loading: boolean = false;
constructor() {
super();
this.bringToFrontOnClick = getUIConfig('bringWindowToFrontOnClick');
this.on(FGUIEvents.DISPLAY, this.onWindowShown, this);
this.on(FGUIEvents.REMOVED_FROM_STAGE, this.onWindowHidden, this);
this.on(FGUIEvents.TOUCH_BEGIN, this.onMouseDown, this);
}
/**
* Add UI source for lazy loading
* 添加用于懒加载的 UI 源
*/
public addUISource(source: IUISource): void {
this._uiSources.push(source);
}
/**
* Get content pane
* 获取内容面板
*/
public get contentPane(): GComponent | null {
return this._contentPane;
}
/**
* Set content pane
* 设置内容面板
*/
public set contentPane(value: GComponent | null) {
if (this._contentPane !== value) {
if (this._contentPane) {
this.removeChild(this._contentPane);
}
this._contentPane = value;
if (this._contentPane) {
this.addChild(this._contentPane);
this.setSize(this._contentPane.width, this._contentPane.height);
this._contentPane.relations.add(this, ERelationType.Size);
this._frame = this._contentPane.getChild('frame') as GComponent | null;
if (this._frame) {
this.closeButton = this._frame.getChild('closeButton');
this.dragArea = this._frame.getChild('dragArea');
this.contentArea = this._frame.getChild('contentArea');
}
}
}
}
/**
* Get frame component
* 获取框架组件
*/
public get frame(): GComponent | null {
return this._frame;
}
/**
* Get close button
* 获取关闭按钮
*/
public get closeButton(): GObject | null {
return this._closeButton;
}
/**
* Set close button
* 设置关闭按钮
*/
public set closeButton(value: GObject | null) {
if (this._closeButton) {
this._closeButton.off(FGUIEvents.CLICK, this.closeEventHandler, this);
}
this._closeButton = value;
if (this._closeButton) {
this._closeButton.on(FGUIEvents.CLICK, this.closeEventHandler, this);
}
}
/**
* Get drag area
* 获取拖拽区域
*/
public get dragArea(): GObject | null {
return this._dragArea;
}
/**
* Set drag area
* 设置拖拽区域
*/
public set dragArea(value: GObject | null) {
if (this._dragArea !== value) {
if (this._dragArea) {
this._dragArea.draggable = false;
this._dragArea.off(FGUIEvents.DRAG_START, this.onDragStart, this);
}
this._dragArea = value;
if (this._dragArea) {
if (this._dragArea instanceof GGraph) {
this._dragArea.drawRect(0, 'transparent', 'transparent');
}
this._dragArea.draggable = true;
this._dragArea.on(FGUIEvents.DRAG_START, this.onDragStart, this);
}
}
}
/**
* Get content area
* 获取内容区域
*/
public get contentArea(): GObject | null {
return this._contentArea;
}
/**
* Set content area
* 设置内容区域
*/
public set contentArea(value: GObject | null) {
this._contentArea = value;
}
/**
* Show window on default GRoot
* 在默认 GRoot 上显示窗口
*/
public show(): void {
GRoot.inst.showWindow(this);
}
/**
* Show window on specified GRoot
* 在指定 GRoot 上显示窗口
*/
public showOn(root: GRoot): void {
root.showWindow(this);
}
/**
* Hide window with animation
* 隐藏窗口(带动画)
*/
public hide(): void {
if (this.isShowing) {
this.doHideAnimation();
}
}
/**
* Hide window immediately
* 立即隐藏窗口
*/
public hideImmediately(): void {
const r = this.parent instanceof GRoot ? this.parent : GRoot.inst;
r.hideWindowImmediately(this);
}
/**
* Center window on GRoot
* 在 GRoot 上居中窗口
*/
public centerOn(r: GRoot, bRestraint?: boolean): void {
this.setXY(
Math.round((r.width - this.width) / 2),
Math.round((r.height - this.height) / 2)
);
if (bRestraint) {
this.relations.add(r, ERelationType.CenterCenter);
this.relations.add(r, ERelationType.MiddleMiddle);
}
}
/**
* Toggle window visibility
* 切换窗口可见性
*/
public toggleStatus(): void {
if (this.isTop) {
this.hide();
} else {
this.show();
}
}
/**
* Check if window is showing
* 检查窗口是否正在显示
*/
public get isShowing(): boolean {
return this.parent !== null;
}
/**
* Check if window is on top
* 检查窗口是否在最上层
*/
public get isTop(): boolean {
return (
this.parent !== null &&
this.parent.getChildIndex(this) === this.parent.numChildren - 1
);
}
/**
* Get modal state
* 获取模态状态
*/
public get modal(): boolean {
return this._modal;
}
/**
* Set modal state
* 设置模态状态
*/
public set modal(value: boolean) {
this._modal = value;
}
/**
* Bring window to front
* 将窗口置于最前
*/
public bringToFront(): void {
this.root?.bringToFront(this);
}
/**
* Show modal wait indicator
* 显示模态等待指示器
*/
public showModalWait(requestingCmd?: number): void {
if (requestingCmd !== undefined) {
this._requestingCmd = requestingCmd;
}
const modalWaitingUrl = getUIConfig('windowModalWaiting');
if (modalWaitingUrl) {
if (!this._modalWaitPane) {
this._modalWaitPane = UIPackage.createObjectFromURL(modalWaitingUrl);
}
if (this._modalWaitPane) {
this.layoutModalWaitPane();
this.addChild(this._modalWaitPane);
}
}
}
/**
* Layout modal wait pane
* 布局模态等待面板
*/
protected layoutModalWaitPane(): void {
if (!this._modalWaitPane) return;
if (this._contentArea && this._frame) {
const pt = this._frame.localToGlobal(0, 0);
const localPt = this.globalToLocal(pt.x, pt.y);
this._modalWaitPane.setXY(
localPt.x + this._contentArea.x,
localPt.y + this._contentArea.y
);
this._modalWaitPane.setSize(this._contentArea.width, this._contentArea.height);
} else {
this._modalWaitPane.setSize(this.width, this.height);
}
}
/**
* Close modal wait indicator
* 关闭模态等待指示器
*/
public closeModalWait(requestingCmd?: number): boolean {
if (requestingCmd !== undefined) {
if (this._requestingCmd !== requestingCmd) {
return false;
}
}
this._requestingCmd = 0;
if (this._modalWaitPane?.parent) {
this.removeChild(this._modalWaitPane);
}
return true;
}
/**
* Check if modal waiting
* 检查是否正在模态等待
*/
public get modalWaiting(): boolean {
return this._modalWaitPane?.parent !== null && this._modalWaitPane?.parent !== undefined;
}
/**
* Initialize window
* 初始化窗口
*/
public init(): void {
if (this._inited || this._loading) {
return;
}
if (this._uiSources.length > 0) {
this._loading = false;
for (const source of this._uiSources) {
if (!source.loaded) {
source.load(this.onUILoadComplete.bind(this), this);
this._loading = true;
}
}
if (!this._loading) {
this.doInit();
}
} else {
this.doInit();
}
}
/**
* Called when window is initialized
* 窗口初始化时调用
*/
protected onInit(): void {
// Override in subclass
}
/**
* Called when window is shown
* 窗口显示时调用
*/
protected onShown(): void {
// Override in subclass
}
/**
* Called when window is hidden
* 窗口隐藏时调用
*/
protected onHide(): void {
// Override in subclass
}
/**
* Perform show animation
* 执行显示动画
*/
protected doShowAnimation(): void {
this.onShown();
}
/**
* Perform hide animation
* 执行隐藏动画
*/
protected doHideAnimation(): void {
this.hideImmediately();
}
private onUILoadComplete(): void {
for (const source of this._uiSources) {
if (!source.loaded) {
return;
}
}
this._loading = false;
this.doInit();
}
private doInit(): void {
this._inited = true;
this.onInit();
if (this.isShowing) {
this.doShowAnimation();
}
}
public dispose(): void {
if (this.parent) {
this.hideImmediately();
}
super.dispose();
}
/**
* Close button event handler
* 关闭按钮事件处理
*/
protected closeEventHandler(): void {
this.hide();
}
private onWindowShown(): void {
if (!this._inited) {
this.init();
} else {
this.doShowAnimation();
}
}
private onWindowHidden(): void {
this.closeModalWait();
this.onHide();
}
private onMouseDown(): void {
if (this.isShowing && this.bringToFrontOnClick) {
this.bringToFront();
}
}
private onDragStart(): void {
if (this._dragArea) {
this._dragArea.stopDrag();
}
this.startDrag();
}
}

View File

@@ -0,0 +1,10 @@
export { GImage } from './GImage';
export { GTextField } from './GTextField';
export { GGraph } from './GGraph';
export { GButton } from './GButton';
export { GProgressBar } from './GProgressBar';
export { GSlider } from './GSlider';
export { GLoader } from './GLoader';
export { GList } from './GList';
export type { ItemRenderer, ItemProvider } from './GList';
export { GTextInput, EKeyboardType } from './GTextInput';