* 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
507 lines
13 KiB
TypeScript
507 lines
13 KiB
TypeScript
import { GComponent } from './GComponent';
|
||
import { GObject } from './GObject';
|
||
import { Stage } from './Stage';
|
||
import { Timer } from './Timer';
|
||
import { FGUIEvents, IInputEventData } from '../events/Events';
|
||
import type { IRenderCollector } from '../render/IRenderCollector';
|
||
|
||
/**
|
||
* GRoot
|
||
*
|
||
* Root container for all UI elements.
|
||
* Manages focus, popups, tooltips, and input dispatch.
|
||
*
|
||
* 所有 UI 元素的根容器,管理焦点、弹出窗口、提示和输入分发
|
||
*/
|
||
export class GRoot extends GComponent {
|
||
private static _inst: GRoot | null = null;
|
||
|
||
private _focus: GObject | null = null;
|
||
private _tooltipWin: GObject | null = null;
|
||
private _defaultTooltipWin: GObject | null = null;
|
||
|
||
private _popupStack: GObject[] = [];
|
||
private _justClosedPopups: GObject[] = [];
|
||
private _modalLayer: GObject | null = null;
|
||
private _modalWaitPane: GObject | null = null;
|
||
|
||
private _inputProcessor: InputProcessor;
|
||
|
||
constructor() {
|
||
super();
|
||
|
||
this._inputProcessor = new InputProcessor(this);
|
||
|
||
// Set this as stage root so children receive addedToStage events
|
||
// 将自己设置为舞台根,这样子对象才能收到 addedToStage 事件
|
||
if (this.displayObject) {
|
||
this.displayObject.setStage(this.displayObject);
|
||
}
|
||
|
||
// Bind to stage events
|
||
const stage = Stage.inst;
|
||
stage.on('mousedown', this.onStageMouseDown, this);
|
||
stage.on('mouseup', this.onStageMouseUp, this);
|
||
stage.on('mousemove', this.onStageMouseMove, this);
|
||
stage.on('wheel', this.onStageWheel, this);
|
||
stage.on('resize', this.onStageResize, this);
|
||
|
||
// Set initial size
|
||
this.setSize(stage.designWidth, stage.designHeight);
|
||
}
|
||
|
||
/**
|
||
* Get singleton instance
|
||
* 获取单例实例
|
||
*/
|
||
public static get inst(): GRoot {
|
||
if (!GRoot._inst) {
|
||
GRoot._inst = new GRoot();
|
||
}
|
||
return GRoot._inst;
|
||
}
|
||
|
||
/**
|
||
* Create a new GRoot (for multi-window support)
|
||
* 创建新的 GRoot(支持多窗口)
|
||
*/
|
||
public static create(): GRoot {
|
||
return new GRoot();
|
||
}
|
||
|
||
// Focus management | 焦点管理
|
||
|
||
/**
|
||
* Get focused object
|
||
* 获取当前焦点对象
|
||
*/
|
||
public get focus(): GObject | null {
|
||
return this._focus;
|
||
}
|
||
|
||
/**
|
||
* Set focused object
|
||
* 设置焦点对象
|
||
*/
|
||
public set focus(value: GObject | null) {
|
||
if (this._focus !== value) {
|
||
const oldFocus = this._focus;
|
||
this._focus = value;
|
||
|
||
if (oldFocus) {
|
||
oldFocus.emit(FGUIEvents.FOCUS_OUT);
|
||
}
|
||
if (this._focus) {
|
||
this._focus.emit(FGUIEvents.FOCUS_IN);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Popup management | 弹出窗口管理
|
||
|
||
/**
|
||
* Show popup at position
|
||
* 在指定位置显示弹出窗口
|
||
*/
|
||
public showPopup(popup: GObject, target?: GObject, dir?: number): void {
|
||
if (this._popupStack.indexOf(popup) === -1) {
|
||
this._popupStack.push(popup);
|
||
}
|
||
|
||
this.addChild(popup);
|
||
this.adjustModalLayer();
|
||
|
||
if (target) {
|
||
const pos = target.localToGlobal(0, 0);
|
||
popup.setXY(pos.x, pos.y + target.height);
|
||
}
|
||
|
||
popup.visible = true;
|
||
}
|
||
|
||
/**
|
||
* Toggle popup visibility
|
||
* 切换弹出窗口可见性
|
||
*/
|
||
public togglePopup(popup: GObject, target?: GObject, dir?: number): void {
|
||
if (this._justClosedPopups.indexOf(popup) !== -1) {
|
||
return;
|
||
}
|
||
|
||
if (popup.parent === this && popup.visible) {
|
||
this.hidePopup(popup);
|
||
} else {
|
||
this.showPopup(popup, target, dir);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Hide popup
|
||
* 隐藏弹出窗口
|
||
*/
|
||
public hidePopup(popup?: GObject): void {
|
||
if (popup) {
|
||
const index = this._popupStack.indexOf(popup);
|
||
if (index !== -1) {
|
||
this._popupStack.splice(index, 1);
|
||
this.closePopup(popup);
|
||
}
|
||
} else {
|
||
// Hide all popups
|
||
for (const p of this._popupStack) {
|
||
this.closePopup(p);
|
||
}
|
||
this._popupStack.length = 0;
|
||
}
|
||
}
|
||
|
||
private closePopup(popup: GObject): void {
|
||
popup.visible = false;
|
||
this._justClosedPopups.push(popup);
|
||
|
||
Timer.inst.callLater(this, () => {
|
||
const index = this._justClosedPopups.indexOf(popup);
|
||
if (index !== -1) {
|
||
this._justClosedPopups.splice(index, 1);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Check if popup is showing
|
||
* 检查弹出窗口是否正在显示
|
||
*/
|
||
public hasAnyPopup(): boolean {
|
||
return this._popupStack.length > 0;
|
||
}
|
||
|
||
// Modal management | 模态管理
|
||
|
||
private adjustModalLayer(): void {
|
||
// Adjust modal layer position and visibility
|
||
if (this._modalLayer) {
|
||
let hasModal = false;
|
||
for (let i = this._popupStack.length - 1; i >= 0; i--) {
|
||
// Check if popup is modal
|
||
}
|
||
this._modalLayer.visible = hasModal;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show modal wait
|
||
* 显示模态等待
|
||
*/
|
||
public showModalWait(msg?: string): void {
|
||
if (this._modalWaitPane) {
|
||
this.addChild(this._modalWaitPane);
|
||
this._modalWaitPane.visible = true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Close modal wait
|
||
* 关闭模态等待
|
||
*/
|
||
public closeModalWait(): void {
|
||
if (this._modalWaitPane) {
|
||
this._modalWaitPane.visible = false;
|
||
this._modalWaitPane.removeFromParent();
|
||
}
|
||
}
|
||
|
||
// Tooltip management | 提示管理
|
||
|
||
/**
|
||
* Show tooltip
|
||
* 显示提示
|
||
*/
|
||
public showTooltips(msg: string): void {
|
||
if (!this._defaultTooltipWin) return;
|
||
|
||
this._tooltipWin = this._defaultTooltipWin;
|
||
this._tooltipWin.text = msg;
|
||
this.showTooltipsWin(this._tooltipWin);
|
||
}
|
||
|
||
/**
|
||
* Show custom tooltip window
|
||
* 显示自定义提示窗口
|
||
*/
|
||
public showTooltipsWin(tooltipWin: GObject, position?: { x: number; y: number }): void {
|
||
this._tooltipWin = tooltipWin;
|
||
this.addChild(tooltipWin);
|
||
|
||
if (position) {
|
||
tooltipWin.setXY(position.x, position.y);
|
||
} else {
|
||
const stage = Stage.inst;
|
||
tooltipWin.setXY(stage.mouseX + 10, stage.mouseY + 20);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Hide tooltip
|
||
* 隐藏提示
|
||
*/
|
||
public hideTooltips(): void {
|
||
if (this._tooltipWin) {
|
||
this._tooltipWin.removeFromParent();
|
||
this._tooltipWin = null;
|
||
}
|
||
}
|
||
|
||
// Input handling | 输入处理
|
||
|
||
private onStageMouseDown(data: IInputEventData): void {
|
||
this._inputProcessor.onMouseDown(data);
|
||
|
||
// Close popups if clicking outside
|
||
if (this._popupStack.length > 0) {
|
||
const hit = this.hitTest(data.stageX, data.stageY);
|
||
if (!hit || !this.isAncestorOf(hit, this._popupStack[this._popupStack.length - 1])) {
|
||
this.hidePopup();
|
||
}
|
||
}
|
||
|
||
this.hideTooltips();
|
||
}
|
||
|
||
private onStageMouseUp(data: IInputEventData): void {
|
||
this._inputProcessor.onMouseUp(data);
|
||
}
|
||
|
||
private onStageMouseMove(data: IInputEventData): void {
|
||
this._inputProcessor.onMouseMove(data);
|
||
}
|
||
|
||
private onStageWheel(data: IInputEventData): void {
|
||
this._inputProcessor.onMouseWheel(data);
|
||
}
|
||
|
||
private onStageResize(): void {
|
||
const stage = Stage.inst;
|
||
this.setSize(stage.designWidth, stage.designHeight);
|
||
}
|
||
|
||
private isAncestorOf(obj: GObject, ancestor: GObject): boolean {
|
||
let p: GObject | null = obj;
|
||
while (p) {
|
||
if (p === ancestor) return true;
|
||
p = p.parent;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Hit test at position
|
||
* 位置碰撞检测
|
||
*/
|
||
public hitTest(stageX: number, stageY: number): GObject | null {
|
||
return this._inputProcessor.hitTest(stageX, stageY);
|
||
}
|
||
|
||
// Drag and drop | 拖放
|
||
|
||
/**
|
||
* Start dragging a source object
|
||
* 开始拖拽源对象
|
||
*/
|
||
public startDragSource(source: GObject): void {
|
||
GObject.draggingObject = source;
|
||
}
|
||
|
||
/**
|
||
* Stop dragging
|
||
* 停止拖拽
|
||
*/
|
||
public stopDragSource(): void {
|
||
GObject.draggingObject = null;
|
||
}
|
||
|
||
// Window management | 窗口管理
|
||
|
||
/**
|
||
* Show window
|
||
* 显示窗口
|
||
*/
|
||
public showWindow(win: GObject): void {
|
||
this.addChild(win);
|
||
this.adjustModalLayer();
|
||
}
|
||
|
||
/**
|
||
* Hide window immediately
|
||
* 立即隐藏窗口
|
||
*/
|
||
public hideWindowImmediately(win: GObject): void {
|
||
if (win.parent === this) {
|
||
this.removeChild(win);
|
||
}
|
||
this.adjustModalLayer();
|
||
}
|
||
|
||
/**
|
||
* Bring window to front
|
||
* 将窗口置于最前
|
||
*/
|
||
public bringToFront(win: GObject): void {
|
||
const cnt = this.numChildren;
|
||
let i: number;
|
||
if (this._modalLayer && this._modalLayer.parent === this) {
|
||
i = this.getChildIndex(this._modalLayer);
|
||
} else {
|
||
i = cnt - 1;
|
||
}
|
||
|
||
const index = this.getChildIndex(win);
|
||
if (index < i) {
|
||
this.setChildIndex(win, i);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get top window
|
||
* 获取最上层窗口
|
||
*/
|
||
public getTopWindow(): GObject | null {
|
||
const cnt = this.numChildren;
|
||
for (let i = cnt - 1; i >= 0; i--) {
|
||
const child = this.getChildAt(i);
|
||
if (child !== this._modalLayer) {
|
||
return child;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Update | 更新
|
||
|
||
/**
|
||
* Update GRoot (called each frame by ECS system)
|
||
* 更新 GRoot(每帧由 ECS 系统调用)
|
||
*/
|
||
public update(): void {
|
||
// Update timers
|
||
// Update transitions
|
||
// Update scroll panes
|
||
}
|
||
|
||
// Disposal | 销毁
|
||
|
||
public dispose(): void {
|
||
const stage = Stage.inst;
|
||
stage.off('mousedown', this.onStageMouseDown);
|
||
stage.off('mouseup', this.onStageMouseUp);
|
||
stage.off('mousemove', this.onStageMouseMove);
|
||
stage.off('wheel', this.onStageWheel);
|
||
stage.off('resize', this.onStageResize);
|
||
|
||
this._inputProcessor.dispose();
|
||
|
||
if (GRoot._inst === this) {
|
||
GRoot._inst = null;
|
||
}
|
||
|
||
super.dispose();
|
||
}
|
||
|
||
// Render | 渲染
|
||
|
||
public collectRenderData(collector: IRenderCollector): void {
|
||
super.collectRenderData(collector);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* InputProcessor
|
||
*
|
||
* Handles input event processing and dispatching.
|
||
*
|
||
* 处理输入事件的处理和分发
|
||
*/
|
||
class InputProcessor {
|
||
private _root: GRoot;
|
||
private _touchTarget: GObject | null = null;
|
||
private _rollOverTarget: GObject | null = null;
|
||
|
||
constructor(root: GRoot) {
|
||
this._root = root;
|
||
}
|
||
|
||
public hitTest(stageX: number, stageY: number): GObject | null {
|
||
return this.hitTestInChildren(this._root, stageX, stageY);
|
||
}
|
||
|
||
private hitTestInChildren(container: GComponent, stageX: number, stageY: number): GObject | null {
|
||
const count = container.numChildren;
|
||
for (let i = count - 1; i >= 0; i--) {
|
||
const child = container.getChildAt(i);
|
||
if (!child.visible || !child.touchable) continue;
|
||
|
||
const local = child.globalToLocal(stageX, stageY);
|
||
if (local.x >= 0 && local.x < child.width && local.y >= 0 && local.y < child.height) {
|
||
if (child instanceof GComponent) {
|
||
const deeper = this.hitTestInChildren(child, stageX, stageY);
|
||
if (deeper) return deeper;
|
||
}
|
||
return child;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
public onMouseDown(data: IInputEventData): void {
|
||
this._touchTarget = this.hitTest(data.stageX, data.stageY);
|
||
if (this._touchTarget) {
|
||
this._root.focus = this._touchTarget;
|
||
this._touchTarget.emit(FGUIEvents.TOUCH_BEGIN, data);
|
||
}
|
||
}
|
||
|
||
public onMouseUp(data: IInputEventData): void {
|
||
if (this._touchTarget) {
|
||
const target = this.hitTest(data.stageX, data.stageY);
|
||
this._touchTarget.emit(FGUIEvents.TOUCH_END, data);
|
||
|
||
if (target === this._touchTarget) {
|
||
this._touchTarget.emit(FGUIEvents.CLICK, data);
|
||
}
|
||
|
||
this._touchTarget = null;
|
||
}
|
||
}
|
||
|
||
public onMouseMove(data: IInputEventData): void {
|
||
const target = this.hitTest(data.stageX, data.stageY);
|
||
|
||
// Handle roll over/out
|
||
if (target !== this._rollOverTarget) {
|
||
if (this._rollOverTarget) {
|
||
this._rollOverTarget.emit(FGUIEvents.ROLL_OUT, data);
|
||
}
|
||
this._rollOverTarget = target;
|
||
if (this._rollOverTarget) {
|
||
this._rollOverTarget.emit(FGUIEvents.ROLL_OVER, data);
|
||
}
|
||
}
|
||
|
||
// Handle touch move
|
||
if (this._touchTarget) {
|
||
this._touchTarget.emit(FGUIEvents.TOUCH_MOVE, data);
|
||
}
|
||
}
|
||
|
||
public onMouseWheel(data: IInputEventData): void {
|
||
const target = this.hitTest(data.stageX, data.stageY);
|
||
if (target) {
|
||
target.emit('wheel', data);
|
||
}
|
||
}
|
||
|
||
public dispose(): void {
|
||
this._touchTarget = null;
|
||
this._rollOverTarget = null;
|
||
}
|
||
}
|