Files
esengine/packages/rendering/fairygui/src/core/GRoot.ts
YHH 155411e743 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
2025-12-26 14:50:35 +08:00

507 lines
13 KiB
TypeScript
Raw Blame History

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