feat(editor): 添加 ECS UI 系统和编辑器更新优化 (#238)
This commit is contained in:
230
packages/ui/src/components/UIInteractableComponent.ts
Normal file
230
packages/ui/src/components/UIInteractableComponent.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 光标类型
|
||||
* Cursor types for interactive elements
|
||||
*/
|
||||
export type UICursorType =
|
||||
| 'default'
|
||||
| 'pointer'
|
||||
| 'text'
|
||||
| 'move'
|
||||
| 'not-allowed'
|
||||
| 'grab'
|
||||
| 'grabbing'
|
||||
| 'ew-resize'
|
||||
| 'ns-resize'
|
||||
| 'nesw-resize'
|
||||
| 'nwse-resize';
|
||||
|
||||
/**
|
||||
* UI 交互组件
|
||||
* UI Interactable Component - Handles input interaction state
|
||||
*
|
||||
* 管理元素的交互状态(悬停、按下、焦点等)
|
||||
* Manages element interaction state (hover, pressed, focus, etc.)
|
||||
*/
|
||||
@ECSComponent('UIInteractable')
|
||||
@Serializable({ version: 1, typeId: 'UIInteractable' })
|
||||
export class UIInteractableComponent extends Component {
|
||||
/**
|
||||
* 是否启用交互
|
||||
* Whether interaction is enabled
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Enabled' })
|
||||
public enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* 是否阻止事件冒泡
|
||||
* Whether to block event propagation
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Block Events' })
|
||||
public blockEvents: boolean = true;
|
||||
|
||||
// ===== 状态 State (由 UIInputSystem 更新) =====
|
||||
|
||||
/**
|
||||
* 是否被鼠标悬停
|
||||
* Whether mouse is hovering over this element
|
||||
*/
|
||||
public hovered: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否被按下
|
||||
* Whether element is being pressed
|
||||
*/
|
||||
public pressed: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否获得焦点
|
||||
* Whether element has focus
|
||||
*/
|
||||
public focused: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否被拖拽
|
||||
* Whether element is being dragged
|
||||
*/
|
||||
public dragging: boolean = false;
|
||||
|
||||
// ===== 配置 Configuration =====
|
||||
|
||||
/**
|
||||
* 是否可以获得焦点
|
||||
* Whether element can receive focus
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Focusable' })
|
||||
public focusable: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否可以被拖拽
|
||||
* Whether element can be dragged
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Draggable' })
|
||||
public draggable: boolean = false;
|
||||
|
||||
/**
|
||||
* Tab 索引(用于键盘导航)
|
||||
* Tab index for keyboard navigation
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Tab Index' })
|
||||
public tabIndex: number = 0;
|
||||
|
||||
/**
|
||||
* 光标类型
|
||||
* Cursor type when hovering
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Cursor',
|
||||
options: [
|
||||
{ value: 'default', label: 'Default' },
|
||||
{ value: 'pointer', label: 'Pointer' },
|
||||
{ value: 'text', label: 'Text' },
|
||||
{ value: 'move', label: 'Move' },
|
||||
{ value: 'not-allowed', label: 'Not Allowed' },
|
||||
{ value: 'grab', label: 'Grab' },
|
||||
{ value: 'grabbing', label: 'Grabbing' }
|
||||
]
|
||||
})
|
||||
public cursor: UICursorType = 'pointer';
|
||||
|
||||
/**
|
||||
* 悬停延迟(毫秒,用于 tooltip)
|
||||
* Hover delay in ms (for tooltips)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Hover Delay', min: 0 })
|
||||
public hoverDelay: number = 0;
|
||||
|
||||
/**
|
||||
* 悬停计时器
|
||||
* Internal hover timer
|
||||
*/
|
||||
public hoverTimer: number = 0;
|
||||
|
||||
/**
|
||||
* 是否悬停足够长时间
|
||||
* Whether hovered long enough (past hoverDelay)
|
||||
*/
|
||||
public hoverReady: boolean = false;
|
||||
|
||||
// ===== 事件回调 Event Callbacks =====
|
||||
|
||||
/**
|
||||
* 点击回调
|
||||
* Click callback
|
||||
*/
|
||||
public onClick?: () => void;
|
||||
|
||||
/**
|
||||
* 双击回调
|
||||
* Double-click callback
|
||||
*/
|
||||
public onDoubleClick?: () => void;
|
||||
|
||||
/**
|
||||
* 鼠标进入回调
|
||||
* Mouse enter callback
|
||||
*/
|
||||
public onMouseEnter?: () => void;
|
||||
|
||||
/**
|
||||
* 鼠标离开回调
|
||||
* Mouse leave callback
|
||||
*/
|
||||
public onMouseLeave?: () => void;
|
||||
|
||||
/**
|
||||
* 按下回调
|
||||
* Press down callback
|
||||
*/
|
||||
public onPressDown?: () => void;
|
||||
|
||||
/**
|
||||
* 释放回调
|
||||
* Press up callback
|
||||
*/
|
||||
public onPressUp?: () => void;
|
||||
|
||||
/**
|
||||
* 获得焦点回调
|
||||
* Focus callback
|
||||
*/
|
||||
public onFocus?: () => void;
|
||||
|
||||
/**
|
||||
* 失去焦点回调
|
||||
* Blur callback
|
||||
*/
|
||||
public onBlur?: () => void;
|
||||
|
||||
/**
|
||||
* 拖拽开始回调
|
||||
* Drag start callback
|
||||
*/
|
||||
public onDragStart?: (x: number, y: number) => void;
|
||||
|
||||
/**
|
||||
* 拖拽中回调
|
||||
* Drag move callback
|
||||
*/
|
||||
public onDragMove?: (x: number, y: number, deltaX: number, deltaY: number) => void;
|
||||
|
||||
/**
|
||||
* 拖拽结束回调
|
||||
* Drag end callback
|
||||
*/
|
||||
public onDragEnd?: (x: number, y: number) => void;
|
||||
|
||||
/**
|
||||
* 获取当前交互状态名称(用于样式切换)
|
||||
* Get current interaction state name (for style switching)
|
||||
*/
|
||||
public getState(): 'disabled' | 'pressed' | 'hovered' | 'focused' | 'normal' {
|
||||
if (!this.enabled) return 'disabled';
|
||||
if (this.pressed) return 'pressed';
|
||||
if (this.hovered) return 'hovered';
|
||||
if (this.focused) return 'focused';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有状态
|
||||
* Reset all interaction states
|
||||
*/
|
||||
public resetState(): void {
|
||||
this.hovered = false;
|
||||
this.pressed = false;
|
||||
this.focused = false;
|
||||
this.dragging = false;
|
||||
this.hoverTimer = 0;
|
||||
this.hoverReady = false;
|
||||
}
|
||||
}
|
||||
373
packages/ui/src/components/UILayoutComponent.ts
Normal file
373
packages/ui/src/components/UILayoutComponent.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 布局类型
|
||||
* Layout types for automatic child positioning
|
||||
*/
|
||||
export enum UILayoutType {
|
||||
/** 无自动布局 No automatic layout */
|
||||
None = 'none',
|
||||
/** 水平排列 Horizontal arrangement */
|
||||
Horizontal = 'horizontal',
|
||||
/** 垂直排列 Vertical arrangement */
|
||||
Vertical = 'vertical',
|
||||
/** 网格布局 Grid layout */
|
||||
Grid = 'grid',
|
||||
/** 流式布局 Flow/Wrap layout */
|
||||
Flow = 'flow'
|
||||
}
|
||||
|
||||
/**
|
||||
* 主轴对齐方式
|
||||
* Main axis alignment
|
||||
*/
|
||||
export enum UIJustifyContent {
|
||||
/** 起始对齐 Align to start */
|
||||
Start = 'start',
|
||||
/** 居中 Center */
|
||||
Center = 'center',
|
||||
/** 末尾对齐 Align to end */
|
||||
End = 'end',
|
||||
/** 两端对齐 Space between */
|
||||
SpaceBetween = 'space-between',
|
||||
/** 均匀分布 Space around */
|
||||
SpaceAround = 'space-around',
|
||||
/** 平均分布 Space evenly */
|
||||
SpaceEvenly = 'space-evenly'
|
||||
}
|
||||
|
||||
/**
|
||||
* 交叉轴对齐方式
|
||||
* Cross axis alignment
|
||||
*/
|
||||
export enum UIAlignItems {
|
||||
/** 起始对齐 Align to start */
|
||||
Start = 'start',
|
||||
/** 居中 Center */
|
||||
Center = 'center',
|
||||
/** 末尾对齐 Align to end */
|
||||
End = 'end',
|
||||
/** 拉伸 Stretch to fill */
|
||||
Stretch = 'stretch',
|
||||
/** 基线对齐 Baseline alignment */
|
||||
Baseline = 'baseline'
|
||||
}
|
||||
|
||||
/**
|
||||
* 内边距
|
||||
* Padding configuration
|
||||
*/
|
||||
export interface UIPadding {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 布局组件
|
||||
* UI Layout Component - Defines automatic child layout behavior
|
||||
*
|
||||
* 类似 CSS Flexbox 的布局系统
|
||||
* Flexbox-like layout system
|
||||
*/
|
||||
@ECSComponent('UILayout')
|
||||
@Serializable({ version: 1, typeId: 'UILayout' })
|
||||
export class UILayoutComponent extends Component {
|
||||
/**
|
||||
* 布局类型
|
||||
* Layout type
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Type',
|
||||
options: [
|
||||
{ value: 'none', label: 'None' },
|
||||
{ value: 'horizontal', label: 'Horizontal' },
|
||||
{ value: 'vertical', label: 'Vertical' },
|
||||
{ value: 'grid', label: 'Grid' },
|
||||
{ value: 'flow', label: 'Flow' }
|
||||
]
|
||||
})
|
||||
public type: UILayoutType = UILayoutType.None;
|
||||
|
||||
// ===== 间距 Spacing =====
|
||||
|
||||
/**
|
||||
* 子元素间距
|
||||
* Gap between children
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Gap', min: 0 })
|
||||
public gap: number = 0;
|
||||
|
||||
/**
|
||||
* 水平间距(Grid 布局)
|
||||
* Horizontal gap (for Grid layout)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Gap X', min: 0 })
|
||||
public gapX: number = 0;
|
||||
|
||||
/**
|
||||
* 垂直间距(Grid 布局)
|
||||
* Vertical gap (for Grid layout)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Gap Y', min: 0 })
|
||||
public gapY: number = 0;
|
||||
|
||||
/**
|
||||
* 内边距
|
||||
* Padding
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Padding Top', min: 0 })
|
||||
public paddingTop: number = 0;
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Padding Right', min: 0 })
|
||||
public paddingRight: number = 0;
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Padding Bottom', min: 0 })
|
||||
public paddingBottom: number = 0;
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Padding Left', min: 0 })
|
||||
public paddingLeft: number = 0;
|
||||
|
||||
// ===== 对齐 Alignment =====
|
||||
|
||||
/**
|
||||
* 主轴对齐
|
||||
* Main axis alignment (justify-content)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Justify Content',
|
||||
options: [
|
||||
{ value: 'start', label: 'Start' },
|
||||
{ value: 'center', label: 'Center' },
|
||||
{ value: 'end', label: 'End' },
|
||||
{ value: 'space-between', label: 'Space Between' },
|
||||
{ value: 'space-around', label: 'Space Around' },
|
||||
{ value: 'space-evenly', label: 'Space Evenly' }
|
||||
]
|
||||
})
|
||||
public justifyContent: UIJustifyContent = UIJustifyContent.Start;
|
||||
|
||||
/**
|
||||
* 交叉轴对齐
|
||||
* Cross axis alignment (align-items)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Align Items',
|
||||
options: [
|
||||
{ value: 'start', label: 'Start' },
|
||||
{ value: 'center', label: 'Center' },
|
||||
{ value: 'end', label: 'End' },
|
||||
{ value: 'stretch', label: 'Stretch' },
|
||||
{ value: 'baseline', label: 'Baseline' }
|
||||
]
|
||||
})
|
||||
public alignItems: UIAlignItems = UIAlignItems.Start;
|
||||
|
||||
// ===== 网格配置 Grid Configuration =====
|
||||
|
||||
/**
|
||||
* 网格列数
|
||||
* Number of columns (Grid layout)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Columns', min: 1 })
|
||||
public columns: number = 1;
|
||||
|
||||
/**
|
||||
* 网格行数(0 = 自动)
|
||||
* Number of rows (Grid layout, 0 = auto)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Rows', min: 0 })
|
||||
public rows: number = 0;
|
||||
|
||||
/**
|
||||
* 网格单元格宽度(0 = 自动)
|
||||
* Grid cell width (0 = auto)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Cell Width', min: 0 })
|
||||
public cellWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 网格单元格高度(0 = 自动)
|
||||
* Grid cell height (0 = auto)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Cell Height', min: 0 })
|
||||
public cellHeight: number = 0;
|
||||
|
||||
// ===== 流式布局配置 Flow Configuration =====
|
||||
|
||||
/**
|
||||
* 是否换行
|
||||
* Whether to wrap items
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Wrap' })
|
||||
public wrap: boolean = false;
|
||||
|
||||
/**
|
||||
* 换行时的行间距
|
||||
* Gap between wrapped rows
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Wrap Gap', min: 0 })
|
||||
public wrapGap: number = 0;
|
||||
|
||||
// ===== 方向 Direction =====
|
||||
|
||||
/**
|
||||
* 是否反转方向
|
||||
* Whether to reverse direction
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Reverse' })
|
||||
public reverse: boolean = false;
|
||||
|
||||
// ===== 尺寸控制 Size Control =====
|
||||
|
||||
/**
|
||||
* 是否根据内容调整自身尺寸
|
||||
* Whether to fit size to content
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Fit Content' })
|
||||
public fitContent: boolean = false;
|
||||
|
||||
/**
|
||||
* 内容最小宽度
|
||||
* Minimum content width
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Content Min Width', min: 0 })
|
||||
public contentMinWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 内容最小高度
|
||||
* Minimum content height
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Content Min Height', min: 0 })
|
||||
public contentMinHeight: number = 0;
|
||||
|
||||
/**
|
||||
* 设置布局类型
|
||||
* Set layout type
|
||||
*/
|
||||
public setType(type: UILayoutType): this {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置间距
|
||||
* Set gap
|
||||
*/
|
||||
public setGap(gap: number, gapY?: number): this {
|
||||
this.gap = gap;
|
||||
this.gapX = gap;
|
||||
this.gapY = gapY ?? gap;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内边距
|
||||
* Set padding (uniform or per-side)
|
||||
*/
|
||||
public setPadding(padding: number | UIPadding): this {
|
||||
if (typeof padding === 'number') {
|
||||
this.paddingTop = padding;
|
||||
this.paddingRight = padding;
|
||||
this.paddingBottom = padding;
|
||||
this.paddingLeft = padding;
|
||||
} else {
|
||||
this.paddingTop = padding.top;
|
||||
this.paddingRight = padding.right;
|
||||
this.paddingBottom = padding.bottom;
|
||||
this.paddingLeft = padding.left;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置对齐方式
|
||||
* Set alignment
|
||||
*/
|
||||
public setAlignment(justify: UIJustifyContent, align: UIAlignItems): this {
|
||||
this.justifyContent = justify;
|
||||
this.alignItems = align;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置网格配置
|
||||
* Set grid configuration
|
||||
*/
|
||||
public setGrid(columns: number, cellWidth?: number, cellHeight?: number): this {
|
||||
this.type = UILayoutType.Grid;
|
||||
this.columns = columns;
|
||||
if (cellWidth !== undefined) this.cellWidth = cellWidth;
|
||||
if (cellHeight !== undefined) this.cellHeight = cellHeight;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效的水平间距
|
||||
* Get effective horizontal gap
|
||||
*/
|
||||
public getHorizontalGap(): number {
|
||||
return this.gapX || this.gap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效的垂直间距
|
||||
* Get effective vertical gap
|
||||
*/
|
||||
public getVerticalGap(): number {
|
||||
return this.gapY || this.gap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容区域起始 X
|
||||
* Get content area start X
|
||||
*/
|
||||
public getContentStartX(): number {
|
||||
return this.paddingLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容区域起始 Y
|
||||
* Get content area start Y
|
||||
*/
|
||||
public getContentStartY(): number {
|
||||
return this.paddingTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内边距水平总和
|
||||
* Get total horizontal padding
|
||||
*/
|
||||
public getHorizontalPadding(): number {
|
||||
return this.paddingLeft + this.paddingRight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内边距垂直总和
|
||||
* Get total vertical padding
|
||||
*/
|
||||
public getVerticalPadding(): number {
|
||||
return this.paddingTop + this.paddingBottom;
|
||||
}
|
||||
}
|
||||
303
packages/ui/src/components/UIRenderComponent.ts
Normal file
303
packages/ui/src/components/UIRenderComponent.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 渲染类型
|
||||
* Render types for different visual elements
|
||||
*/
|
||||
export enum UIRenderType {
|
||||
/** 纯色矩形 Solid color rectangle */
|
||||
Rect = 'rect',
|
||||
/** 图片 Image/Texture */
|
||||
Image = 'image',
|
||||
/** 九宫格图片 Nine-patch/Nine-slice image */
|
||||
NinePatch = 'ninepatch',
|
||||
/** 圆形 Circle */
|
||||
Circle = 'circle',
|
||||
/** 圆角矩形 Rounded rectangle */
|
||||
RoundedRect = 'rounded-rect'
|
||||
}
|
||||
|
||||
/**
|
||||
* 边框样式
|
||||
* Border style configuration
|
||||
*/
|
||||
export interface UIBorderStyle {
|
||||
width: number;
|
||||
color: number;
|
||||
alpha: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 阴影样式
|
||||
* Shadow style configuration
|
||||
*/
|
||||
export interface UIShadowStyle {
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
blur: number;
|
||||
color: number;
|
||||
alpha: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 渲染组件
|
||||
* UI Render Component - Handles visual appearance of UI elements
|
||||
*
|
||||
* 定义元素的视觉属性,如颜色、纹理、边框等
|
||||
* Defines visual properties like color, texture, border, etc.
|
||||
*/
|
||||
@ECSComponent('UIRender')
|
||||
@Serializable({ version: 1, typeId: 'UIRender' })
|
||||
export class UIRenderComponent extends Component {
|
||||
/**
|
||||
* 渲染类型
|
||||
* Type of rendering
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Type',
|
||||
options: [
|
||||
{ value: 'rect', label: 'Rectangle' },
|
||||
{ value: 'image', label: 'Image' },
|
||||
{ value: 'ninepatch', label: 'Nine Patch' },
|
||||
{ value: 'circle', label: 'Circle' },
|
||||
{ value: 'rounded-rect', label: 'Rounded Rect' }
|
||||
]
|
||||
})
|
||||
public type: UIRenderType = UIRenderType.Rect;
|
||||
|
||||
// ===== 颜色 Colors =====
|
||||
|
||||
/**
|
||||
* 背景颜色 (0xRRGGBB)
|
||||
* Background color in hex format
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Background Color' })
|
||||
public backgroundColor: number = 0xFFFFFF;
|
||||
|
||||
/**
|
||||
* 背景透明度 (0-1)
|
||||
* Background alpha
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Background Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public backgroundAlpha: number = 1;
|
||||
|
||||
/**
|
||||
* 是否填充背景
|
||||
* Whether to fill background
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Fill Background' })
|
||||
public fillBackground: boolean = true;
|
||||
|
||||
// ===== 纹理 Texture =====
|
||||
|
||||
/**
|
||||
* 纹理路径或 ID
|
||||
* Texture path or runtime ID
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Texture', assetType: 'texture' })
|
||||
public texture: string | number | null = null;
|
||||
|
||||
/**
|
||||
* 纹理 UV 坐标 (用于图集)
|
||||
* Texture UV coordinates (for atlas)
|
||||
*/
|
||||
public textureUV: { u0: number; v0: number; u1: number; v1: number } | null = null;
|
||||
|
||||
/**
|
||||
* 纹理色调 (0xRRGGBB)
|
||||
* Texture tint color
|
||||
*/
|
||||
public textureTint: number = 0xFFFFFF;
|
||||
|
||||
// ===== 九宫格 Nine-Patch =====
|
||||
|
||||
/**
|
||||
* 九宫格边距 [top, right, bottom, left]
|
||||
* Nine-patch margins
|
||||
*/
|
||||
public ninePatchMargins: [number, number, number, number] = [0, 0, 0, 0];
|
||||
|
||||
// ===== 边框 Border =====
|
||||
|
||||
/**
|
||||
* 边框宽度
|
||||
* Border width
|
||||
*/
|
||||
@Property({ type: 'number', label: 'Border Width', min: 0 })
|
||||
public borderWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 边框颜色
|
||||
* Border color
|
||||
*/
|
||||
@Property({ type: 'color', label: 'Border Color' })
|
||||
public borderColor: number = 0x000000;
|
||||
|
||||
/**
|
||||
* 边框透明度
|
||||
* Border alpha
|
||||
*/
|
||||
@Property({ type: 'number', label: 'Border Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public borderAlpha: number = 1;
|
||||
|
||||
/**
|
||||
* 圆角半径 [topLeft, topRight, bottomRight, bottomLeft]
|
||||
* Corner radius for each corner
|
||||
*/
|
||||
public borderRadius: [number, number, number, number] = [0, 0, 0, 0];
|
||||
|
||||
// ===== 阴影 Shadow =====
|
||||
|
||||
/**
|
||||
* 是否启用阴影
|
||||
* Whether shadow is enabled
|
||||
*/
|
||||
@Property({ type: 'boolean', label: 'Shadow Enabled' })
|
||||
public shadowEnabled: boolean = false;
|
||||
|
||||
/**
|
||||
* 阴影 X 偏移
|
||||
* Shadow X offset
|
||||
*/
|
||||
@Property({ type: 'number', label: 'Shadow Offset X' })
|
||||
public shadowOffsetX: number = 0;
|
||||
|
||||
/**
|
||||
* 阴影 Y 偏移
|
||||
* Shadow Y offset
|
||||
*/
|
||||
@Property({ type: 'number', label: 'Shadow Offset Y' })
|
||||
public shadowOffsetY: number = 2;
|
||||
|
||||
/**
|
||||
* 阴影模糊半径
|
||||
* Shadow blur radius
|
||||
*/
|
||||
@Property({ type: 'number', label: 'Shadow Blur', min: 0 })
|
||||
public shadowBlur: number = 4;
|
||||
|
||||
/**
|
||||
* 阴影颜色
|
||||
* Shadow color
|
||||
*/
|
||||
@Property({ type: 'color', label: 'Shadow Color' })
|
||||
public shadowColor: number = 0x000000;
|
||||
|
||||
/**
|
||||
* 阴影透明度
|
||||
* Shadow alpha
|
||||
*/
|
||||
@Property({ type: 'number', label: 'Shadow Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public shadowAlpha: number = 0.3;
|
||||
|
||||
// ===== 渐变 Gradient =====
|
||||
|
||||
/**
|
||||
* 渐变类型
|
||||
* Gradient type
|
||||
*/
|
||||
public gradientType: 'none' | 'linear' | 'radial' = 'none';
|
||||
|
||||
/**
|
||||
* 渐变角度(线性渐变)
|
||||
* Gradient angle for linear gradient
|
||||
*/
|
||||
public gradientAngle: number = 0;
|
||||
|
||||
/**
|
||||
* 渐变颜色停止点 [[position, color, alpha], ...]
|
||||
* Gradient color stops
|
||||
*/
|
||||
public gradientStops: Array<[number, number, number]> = [];
|
||||
|
||||
/**
|
||||
* 设置纯色背景
|
||||
* Set solid color background
|
||||
*/
|
||||
public setColor(color: number, alpha: number = 1): this {
|
||||
this.backgroundColor = color;
|
||||
this.backgroundAlpha = alpha;
|
||||
this.fillBackground = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图片
|
||||
* Set image texture
|
||||
*/
|
||||
public setImage(texture: string | number): this {
|
||||
this.type = UIRenderType.Image;
|
||||
this.texture = texture;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置九宫格
|
||||
* Set nine-patch image
|
||||
*/
|
||||
public setNinePatch(texture: string | number, margins: [number, number, number, number]): this {
|
||||
this.type = UIRenderType.NinePatch;
|
||||
this.texture = texture;
|
||||
this.ninePatchMargins = margins;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置边框
|
||||
* Set border style
|
||||
*/
|
||||
public setBorder(width: number, color: number, alpha: number = 1): this {
|
||||
this.borderWidth = width;
|
||||
this.borderColor = color;
|
||||
this.borderAlpha = alpha;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置圆角
|
||||
* Set corner radius (uniform or per-corner)
|
||||
*/
|
||||
public setCornerRadius(radius: number | [number, number, number, number]): this {
|
||||
if (typeof radius === 'number') {
|
||||
this.borderRadius = [radius, radius, radius, radius];
|
||||
} else {
|
||||
this.borderRadius = radius;
|
||||
}
|
||||
const hasRadius = typeof radius === 'number' ? radius > 0 : radius.some(r => r > 0);
|
||||
if (hasRadius) {
|
||||
this.type = UIRenderType.RoundedRect;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置阴影
|
||||
* Set shadow style
|
||||
*/
|
||||
public setShadow(offsetX: number, offsetY: number, blur: number, color: number, alpha: number = 0.3): this {
|
||||
this.shadowEnabled = true;
|
||||
this.shadowOffsetX = offsetX;
|
||||
this.shadowOffsetY = offsetY;
|
||||
this.shadowBlur = blur;
|
||||
this.shadowColor = color;
|
||||
this.shadowAlpha = alpha;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置线性渐变
|
||||
* Set linear gradient
|
||||
*/
|
||||
public setLinearGradient(angle: number, stops: Array<[number, number, number]>): this {
|
||||
this.gradientType = 'linear';
|
||||
this.gradientAngle = angle;
|
||||
this.gradientStops = stops;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
344
packages/ui/src/components/UITextComponent.ts
Normal file
344
packages/ui/src/components/UITextComponent.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 文本对齐方式
|
||||
* Text alignment options
|
||||
*/
|
||||
export type UITextAlign = 'left' | 'center' | 'right';
|
||||
|
||||
/**
|
||||
* 文本垂直对齐方式
|
||||
* Text vertical alignment options
|
||||
*/
|
||||
export type UITextVerticalAlign = 'top' | 'middle' | 'bottom';
|
||||
|
||||
/**
|
||||
* 文本溢出处理
|
||||
* Text overflow handling
|
||||
*/
|
||||
export type UITextOverflow = 'visible' | 'hidden' | 'ellipsis' | 'clip';
|
||||
|
||||
/**
|
||||
* 字体粗细
|
||||
* Font weight options
|
||||
*/
|
||||
export type UIFontWeight = 'normal' | 'bold' | 'lighter' | 'bolder' | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
|
||||
|
||||
/**
|
||||
* UI 文本组件
|
||||
* UI Text Component - Handles text rendering
|
||||
*
|
||||
* 定义文本内容和样式
|
||||
* Defines text content and style
|
||||
*/
|
||||
@ECSComponent('UIText')
|
||||
@Serializable({ version: 1, typeId: 'UIText' })
|
||||
export class UITextComponent extends Component {
|
||||
/**
|
||||
* 文本内容
|
||||
* Text content
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'string', label: 'Text' })
|
||||
public text: string = '';
|
||||
|
||||
// ===== 字体 Font =====
|
||||
|
||||
/**
|
||||
* 字体大小(像素)
|
||||
* Font size in pixels
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Font Size', min: 1 })
|
||||
public fontSize: number = 14;
|
||||
|
||||
/**
|
||||
* 字体族
|
||||
* Font family
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'string', label: 'Font Family' })
|
||||
public fontFamily: string = 'Arial, sans-serif';
|
||||
|
||||
/**
|
||||
* 字体粗细
|
||||
* Font weight
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Font Weight',
|
||||
options: [
|
||||
{ value: 'normal', label: 'Normal' },
|
||||
{ value: 'bold', label: 'Bold' },
|
||||
{ value: 'lighter', label: 'Lighter' },
|
||||
{ value: 'bolder', label: 'Bolder' }
|
||||
]
|
||||
})
|
||||
public fontWeight: UIFontWeight = 'normal';
|
||||
|
||||
/**
|
||||
* 是否斜体
|
||||
* Whether italic
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Italic' })
|
||||
public italic: boolean = false;
|
||||
|
||||
// ===== 颜色 Color =====
|
||||
|
||||
/**
|
||||
* 文本颜色 (0xRRGGBB)
|
||||
* Text color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Color' })
|
||||
public color: number = 0x000000;
|
||||
|
||||
/**
|
||||
* 文本透明度
|
||||
* Text alpha
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public alpha: number = 1;
|
||||
|
||||
// ===== 对齐 Alignment =====
|
||||
|
||||
/**
|
||||
* 水平对齐
|
||||
* Horizontal alignment
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Align',
|
||||
options: [
|
||||
{ value: 'left', label: 'Left' },
|
||||
{ value: 'center', label: 'Center' },
|
||||
{ value: 'right', label: 'Right' }
|
||||
]
|
||||
})
|
||||
public align: UITextAlign = 'left';
|
||||
|
||||
/**
|
||||
* 垂直对齐
|
||||
* Vertical alignment
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Vertical Align',
|
||||
options: [
|
||||
{ value: 'top', label: 'Top' },
|
||||
{ value: 'middle', label: 'Middle' },
|
||||
{ value: 'bottom', label: 'Bottom' }
|
||||
]
|
||||
})
|
||||
public verticalAlign: UITextVerticalAlign = 'middle';
|
||||
|
||||
// ===== 换行 Wrapping =====
|
||||
|
||||
/**
|
||||
* 是否自动换行
|
||||
* Whether to wrap text
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Word Wrap' })
|
||||
public wordWrap: boolean = false;
|
||||
|
||||
/**
|
||||
* 换行宽度(0 = 使用父元素宽度)
|
||||
* Wrap width (0 = use parent width)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Wrap Width', min: 0 })
|
||||
public wrapWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 行高(倍数,1 = 正常)
|
||||
* Line height multiplier
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Line Height', min: 0.1, step: 0.1 })
|
||||
public lineHeight: number = 1.2;
|
||||
|
||||
/**
|
||||
* 字间距
|
||||
* Letter spacing
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Letter Spacing' })
|
||||
public letterSpacing: number = 0;
|
||||
|
||||
// ===== 溢出 Overflow =====
|
||||
|
||||
/**
|
||||
* 文本溢出处理
|
||||
* Text overflow handling
|
||||
*/
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Overflow',
|
||||
options: [
|
||||
{ value: 'visible', label: 'Visible' },
|
||||
{ value: 'hidden', label: 'Hidden' },
|
||||
{ value: 'ellipsis', label: 'Ellipsis' },
|
||||
{ value: 'clip', label: 'Clip' }
|
||||
]
|
||||
})
|
||||
public overflow: UITextOverflow = 'visible';
|
||||
|
||||
/**
|
||||
* 最大显示行数(0 = 无限制)
|
||||
* Maximum number of lines (0 = unlimited)
|
||||
*/
|
||||
@Property({ type: 'integer', label: 'Max Lines', min: 0 })
|
||||
public maxLines: number = 0;
|
||||
|
||||
// ===== 装饰 Decoration =====
|
||||
|
||||
/**
|
||||
* 下划线
|
||||
* Underline
|
||||
*/
|
||||
@Property({ type: 'boolean', label: 'Underline' })
|
||||
public underline: boolean = false;
|
||||
|
||||
/**
|
||||
* 删除线
|
||||
* Strikethrough
|
||||
*/
|
||||
@Property({ type: 'boolean', label: 'Strikethrough' })
|
||||
public strikethrough: boolean = false;
|
||||
|
||||
// ===== 描边 Stroke =====
|
||||
|
||||
/**
|
||||
* 描边宽度
|
||||
* Stroke width
|
||||
*/
|
||||
@Property({ type: 'number', label: 'Stroke Width', min: 0 })
|
||||
public strokeWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 描边颜色
|
||||
* Stroke color
|
||||
*/
|
||||
@Property({ type: 'color', label: 'Stroke Color' })
|
||||
public strokeColor: number = 0x000000;
|
||||
|
||||
// ===== 阴影 Shadow =====
|
||||
|
||||
/**
|
||||
* 文本阴影启用
|
||||
* Text shadow enabled
|
||||
*/
|
||||
@Property({ type: 'boolean', label: 'Shadow' })
|
||||
public shadowEnabled: boolean = false;
|
||||
|
||||
/**
|
||||
* 阴影 X 偏移
|
||||
* Shadow X offset
|
||||
*/
|
||||
public shadowOffsetX: number = 1;
|
||||
|
||||
/**
|
||||
* 阴影 Y 偏移
|
||||
* Shadow Y offset
|
||||
*/
|
||||
public shadowOffsetY: number = 1;
|
||||
|
||||
/**
|
||||
* 阴影颜色
|
||||
* Shadow color
|
||||
*/
|
||||
public shadowColor: number = 0x000000;
|
||||
|
||||
/**
|
||||
* 阴影透明度
|
||||
* Shadow alpha
|
||||
*/
|
||||
public shadowAlpha: number = 0.5;
|
||||
|
||||
// ===== 计算属性 Computed =====
|
||||
|
||||
/**
|
||||
* 计算后的文本行(由渲染系统填充)
|
||||
* Computed text lines (filled by render system)
|
||||
*/
|
||||
public computedLines: string[] = [];
|
||||
|
||||
/**
|
||||
* 计算后的文本宽度
|
||||
* Computed text width
|
||||
*/
|
||||
public computedWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 计算后的文本高度
|
||||
* Computed text height
|
||||
*/
|
||||
public computedHeight: number = 0;
|
||||
|
||||
/**
|
||||
* 文本是否需要重新计算
|
||||
* Whether text needs recomputation
|
||||
*/
|
||||
public dirty: boolean = true;
|
||||
|
||||
/**
|
||||
* 设置文本
|
||||
* Set text content
|
||||
*/
|
||||
public setText(text: string): this {
|
||||
if (this.text !== text) {
|
||||
this.text = text;
|
||||
this.dirty = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字体
|
||||
* Set font properties
|
||||
*/
|
||||
public setFont(size: number, family?: string, weight?: UIFontWeight): this {
|
||||
this.fontSize = size;
|
||||
if (family !== undefined) this.fontFamily = family;
|
||||
if (weight !== undefined) this.fontWeight = weight;
|
||||
this.dirty = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置颜色
|
||||
* Set text color
|
||||
*/
|
||||
public setColor(color: number, alpha: number = 1): this {
|
||||
this.color = color;
|
||||
this.alpha = alpha;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 CSS 字体字符串
|
||||
* Get CSS font string
|
||||
*/
|
||||
public getCSSFont(): string {
|
||||
const style = this.italic ? 'italic ' : '';
|
||||
const weight = typeof this.fontWeight === 'number' ? this.fontWeight : this.fontWeight;
|
||||
return `${style}${weight} ${this.fontSize}px ${this.fontFamily}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取颜色的 CSS 字符串
|
||||
* Get color as CSS string
|
||||
*/
|
||||
public getCSSColor(): string {
|
||||
const r = (this.color >> 16) & 0xFF;
|
||||
const g = (this.color >> 8) & 0xFF;
|
||||
const b = this.color & 0xFF;
|
||||
return `rgba(${r}, ${g}, ${b}, ${this.alpha})`;
|
||||
}
|
||||
}
|
||||
335
packages/ui/src/components/UITransformComponent.ts
Normal file
335
packages/ui/src/components/UITransformComponent.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 锚点预设
|
||||
* Anchor presets for common positioning scenarios
|
||||
*/
|
||||
export enum AnchorPreset {
|
||||
TopLeft = 'top-left',
|
||||
TopCenter = 'top-center',
|
||||
TopRight = 'top-right',
|
||||
MiddleLeft = 'middle-left',
|
||||
MiddleCenter = 'middle-center',
|
||||
MiddleRight = 'middle-right',
|
||||
BottomLeft = 'bottom-left',
|
||||
BottomCenter = 'bottom-center',
|
||||
BottomRight = 'bottom-right',
|
||||
StretchAll = 'stretch-all'
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 变换组件
|
||||
* UI Transform Component - Handles position, size, and hierarchy for UI elements
|
||||
*
|
||||
* 基于父元素的相对定位系统,支持锚点、轴心点和拉伸模式
|
||||
* Relative positioning system based on parent, supports anchors, pivots, and stretch modes
|
||||
*/
|
||||
@ECSComponent('UITransform')
|
||||
@Serializable({ version: 1, typeId: 'UITransform' })
|
||||
export class UITransformComponent extends Component {
|
||||
// ===== 位置 Position =====
|
||||
|
||||
/**
|
||||
* 相对于锚点的 X 偏移
|
||||
* X offset relative to anchor point
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'X' })
|
||||
public x: number = 0;
|
||||
|
||||
/**
|
||||
* 相对于锚点的 Y 偏移
|
||||
* Y offset relative to anchor point
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Y' })
|
||||
public y: number = 0;
|
||||
|
||||
// ===== 尺寸 Size =====
|
||||
|
||||
/**
|
||||
* 宽度(像素或百分比,取决于 widthMode)
|
||||
* Width in pixels or percentage depending on widthMode
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Width', min: 0 })
|
||||
public width: number = 100;
|
||||
|
||||
/**
|
||||
* 高度(像素或百分比,取决于 heightMode)
|
||||
* Height in pixels or percentage depending on heightMode
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Height', min: 0 })
|
||||
public height: number = 30;
|
||||
|
||||
/**
|
||||
* 最小宽度限制
|
||||
* Minimum width constraint
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Min Width', min: 0 })
|
||||
public minWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 最大宽度限制(0 = 无限制)
|
||||
* Maximum width constraint (0 = no limit)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Max Width', min: 0 })
|
||||
public maxWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 最小高度限制
|
||||
* Minimum height constraint
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Min Height', min: 0 })
|
||||
public minHeight: number = 0;
|
||||
|
||||
/**
|
||||
* 最大高度限制(0 = 无限制)
|
||||
* Maximum height constraint (0 = no limit)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Max Height', min: 0 })
|
||||
public maxHeight: number = 0;
|
||||
|
||||
// ===== 锚点 Anchors =====
|
||||
|
||||
/**
|
||||
* 锚点 X 最小值 (0-1),相对于父元素
|
||||
* Anchor X minimum (0-1), relative to parent
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Anchor Min X', min: 0, max: 1, step: 0.01 })
|
||||
public anchorMinX: number = 0;
|
||||
|
||||
/**
|
||||
* 锚点 Y 最小值 (0-1),相对于父元素
|
||||
* Anchor Y minimum (0-1), relative to parent
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Anchor Min Y', min: 0, max: 1, step: 0.01 })
|
||||
public anchorMinY: number = 0;
|
||||
|
||||
/**
|
||||
* 锚点 X 最大值 (0-1),相对于父元素
|
||||
* Anchor X maximum (0-1), relative to parent
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Anchor Max X', min: 0, max: 1, step: 0.01 })
|
||||
public anchorMaxX: number = 0;
|
||||
|
||||
/**
|
||||
* 锚点 Y 最大值 (0-1),相对于父元素
|
||||
* Anchor Y maximum (0-1), relative to parent
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Anchor Max Y', min: 0, max: 1, step: 0.01 })
|
||||
public anchorMaxY: number = 0;
|
||||
|
||||
// ===== 轴心 Pivot =====
|
||||
|
||||
/**
|
||||
* 轴心点 X (0-1),元素自身的旋转/缩放中心
|
||||
* Pivot X (0-1), element's own rotation/scale center
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Pivot X', min: 0, max: 1, step: 0.01 })
|
||||
public pivotX: number = 0.5;
|
||||
|
||||
/**
|
||||
* 轴心点 Y (0-1),元素自身的旋转/缩放中心
|
||||
* Pivot Y (0-1), element's own rotation/scale center
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Pivot Y', min: 0, max: 1, step: 0.01 })
|
||||
public pivotY: number = 0.5;
|
||||
|
||||
// ===== 变换 Transform =====
|
||||
|
||||
/**
|
||||
* 旋转角度(弧度)
|
||||
* Rotation angle in radians
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Rotation', step: 0.01 })
|
||||
public rotation: number = 0;
|
||||
|
||||
/**
|
||||
* X 轴缩放
|
||||
* Scale on X axis
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Scale X', step: 0.01 })
|
||||
public scaleX: number = 1;
|
||||
|
||||
/**
|
||||
* Y 轴缩放
|
||||
* Scale on Y axis
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Scale Y', step: 0.01 })
|
||||
public scaleY: number = 1;
|
||||
|
||||
// ===== 显示 Display =====
|
||||
|
||||
/**
|
||||
* 是否可见
|
||||
* Visibility flag
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Visible' })
|
||||
public visible: boolean = true;
|
||||
|
||||
/**
|
||||
* 渲染层级,值越大越靠前
|
||||
* Render order, higher values render on top
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Z Index' })
|
||||
public zIndex: number = 0;
|
||||
|
||||
/**
|
||||
* 透明度 (0-1)
|
||||
* Opacity (0-1)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public alpha: number = 1;
|
||||
|
||||
// ===== 计算后的世界坐标(由 UILayoutSystem 填充)=====
|
||||
// Computed world coordinates (filled by UILayoutSystem)
|
||||
|
||||
/**
|
||||
* 计算后的世界 X 坐标
|
||||
* Computed world X position
|
||||
*/
|
||||
public worldX: number = 0;
|
||||
|
||||
/**
|
||||
* 计算后的世界 Y 坐标
|
||||
* Computed world Y position
|
||||
*/
|
||||
public worldY: number = 0;
|
||||
|
||||
/**
|
||||
* 计算后的实际宽度
|
||||
* Computed actual width
|
||||
*/
|
||||
public computedWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 计算后的实际高度
|
||||
* Computed actual height
|
||||
*/
|
||||
public computedHeight: number = 0;
|
||||
|
||||
/**
|
||||
* 计算后的世界透明度(考虑父元素透明度)
|
||||
* Computed world alpha (considering parent alpha)
|
||||
*/
|
||||
public worldAlpha: number = 1;
|
||||
|
||||
/**
|
||||
* 布局是否需要更新
|
||||
* Flag indicating layout needs update
|
||||
*/
|
||||
public layoutDirty: boolean = true;
|
||||
|
||||
/**
|
||||
* 设置锚点预设
|
||||
* Set anchor preset for quick positioning
|
||||
*/
|
||||
public setAnchorPreset(preset: AnchorPreset): this {
|
||||
switch (preset) {
|
||||
case AnchorPreset.TopLeft:
|
||||
this.anchorMinX = 0; this.anchorMinY = 0;
|
||||
this.anchorMaxX = 0; this.anchorMaxY = 0;
|
||||
break;
|
||||
case AnchorPreset.TopCenter:
|
||||
this.anchorMinX = 0.5; this.anchorMinY = 0;
|
||||
this.anchorMaxX = 0.5; this.anchorMaxY = 0;
|
||||
break;
|
||||
case AnchorPreset.TopRight:
|
||||
this.anchorMinX = 1; this.anchorMinY = 0;
|
||||
this.anchorMaxX = 1; this.anchorMaxY = 0;
|
||||
break;
|
||||
case AnchorPreset.MiddleLeft:
|
||||
this.anchorMinX = 0; this.anchorMinY = 0.5;
|
||||
this.anchorMaxX = 0; this.anchorMaxY = 0.5;
|
||||
break;
|
||||
case AnchorPreset.MiddleCenter:
|
||||
this.anchorMinX = 0.5; this.anchorMinY = 0.5;
|
||||
this.anchorMaxX = 0.5; this.anchorMaxY = 0.5;
|
||||
break;
|
||||
case AnchorPreset.MiddleRight:
|
||||
this.anchorMinX = 1; this.anchorMinY = 0.5;
|
||||
this.anchorMaxX = 1; this.anchorMaxY = 0.5;
|
||||
break;
|
||||
case AnchorPreset.BottomLeft:
|
||||
this.anchorMinX = 0; this.anchorMinY = 1;
|
||||
this.anchorMaxX = 0; this.anchorMaxY = 1;
|
||||
break;
|
||||
case AnchorPreset.BottomCenter:
|
||||
this.anchorMinX = 0.5; this.anchorMinY = 1;
|
||||
this.anchorMaxX = 0.5; this.anchorMaxY = 1;
|
||||
break;
|
||||
case AnchorPreset.BottomRight:
|
||||
this.anchorMinX = 1; this.anchorMinY = 1;
|
||||
this.anchorMaxX = 1; this.anchorMaxY = 1;
|
||||
break;
|
||||
case AnchorPreset.StretchAll:
|
||||
this.anchorMinX = 0; this.anchorMinY = 0;
|
||||
this.anchorMaxX = 1; this.anchorMaxY = 1;
|
||||
break;
|
||||
}
|
||||
this.layoutDirty = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置位置
|
||||
* Set position
|
||||
*/
|
||||
public setPosition(x: number, y: number): this {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.layoutDirty = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置尺寸
|
||||
* Set size
|
||||
*/
|
||||
public setSize(width: number, height: number): this {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.layoutDirty = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置轴心点
|
||||
* Set pivot point
|
||||
*/
|
||||
public setPivot(x: number, y: number): this {
|
||||
this.pivotX = x;
|
||||
this.pivotY = y;
|
||||
this.layoutDirty = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测点是否在元素内
|
||||
* Test if a point is inside this element
|
||||
*/
|
||||
public containsPoint(worldX: number, worldY: number): boolean {
|
||||
return worldX >= this.worldX &&
|
||||
worldX <= this.worldX + this.computedWidth &&
|
||||
worldY >= this.worldY &&
|
||||
worldY <= this.worldY + this.computedHeight;
|
||||
}
|
||||
}
|
||||
9
packages/ui/src/components/index.ts
Normal file
9
packages/ui/src/components/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Core components
|
||||
export * from './UITransformComponent';
|
||||
export * from './UIRenderComponent';
|
||||
export * from './UIInteractableComponent';
|
||||
export * from './UITextComponent';
|
||||
export * from './UILayoutComponent';
|
||||
|
||||
// Widget components
|
||||
export * from './widgets';
|
||||
311
packages/ui/src/components/widgets/UIButtonComponent.ts
Normal file
311
packages/ui/src/components/widgets/UIButtonComponent.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 按钮状态样式
|
||||
* Button state style configuration
|
||||
*/
|
||||
export interface UIButtonStyle {
|
||||
backgroundColor: number;
|
||||
backgroundAlpha: number;
|
||||
textColor: number;
|
||||
borderColor: number;
|
||||
borderWidth: number;
|
||||
texture?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按钮显示模式
|
||||
* Button display mode
|
||||
*/
|
||||
export type UIButtonDisplayMode = 'color' | 'texture' | 'both';
|
||||
|
||||
/**
|
||||
* UI 按钮组件
|
||||
* UI Button Component - Button-specific state and callbacks
|
||||
*/
|
||||
@ECSComponent('UIButton')
|
||||
@Serializable({ version: 1, typeId: 'UIButton' })
|
||||
export class UIButtonComponent extends Component {
|
||||
/**
|
||||
* 按钮文本
|
||||
* Button label text
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'string', label: 'Label' })
|
||||
public label: string = 'Button';
|
||||
|
||||
// ===== 显示模式 Display Mode =====
|
||||
|
||||
/**
|
||||
* 显示模式:纯颜色、纯纹理、或两者结合
|
||||
* Display mode: color only, texture only, or both
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Display Mode',
|
||||
options: ['color', 'texture', 'both']
|
||||
})
|
||||
public displayMode: UIButtonDisplayMode = 'color';
|
||||
|
||||
// ===== 状态纹理 State Textures =====
|
||||
|
||||
/**
|
||||
* 正常状态纹理
|
||||
* Normal state texture
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Normal Texture', assetType: 'texture' })
|
||||
public normalTexture: string = '';
|
||||
|
||||
/**
|
||||
* 悬停状态纹理
|
||||
* Hover state texture
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Hover Texture', assetType: 'texture' })
|
||||
public hoverTexture: string = '';
|
||||
|
||||
/**
|
||||
* 按下状态纹理
|
||||
* Pressed state texture
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Pressed Texture', assetType: 'texture' })
|
||||
public pressedTexture: string = '';
|
||||
|
||||
/**
|
||||
* 禁用状态纹理
|
||||
* Disabled state texture
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Disabled Texture', assetType: 'texture' })
|
||||
public disabledTexture: string = '';
|
||||
|
||||
// ===== 状态样式 State Styles =====
|
||||
|
||||
/**
|
||||
* 正常状态颜色
|
||||
* Normal state background color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Normal Color' })
|
||||
public normalColor: number = 0x4A90D9;
|
||||
|
||||
/**
|
||||
* 悬停状态颜色
|
||||
* Hover state background color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Hover Color' })
|
||||
public hoverColor: number = 0x5BA0E9;
|
||||
|
||||
/**
|
||||
* 按下状态颜色
|
||||
* Pressed state background color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Pressed Color' })
|
||||
public pressedColor: number = 0x3A80C9;
|
||||
|
||||
/**
|
||||
* 禁用状态颜色
|
||||
* Disabled state background color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Disabled Color' })
|
||||
public disabledColor: number = 0x888888;
|
||||
|
||||
/**
|
||||
* 聚焦状态颜色
|
||||
* Focused state background color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Focused Color' })
|
||||
public focusedColor: number = 0x4A90D9;
|
||||
|
||||
/**
|
||||
* 文本颜色
|
||||
* Text color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Text Color' })
|
||||
public textColor: number = 0xFFFFFF;
|
||||
|
||||
/**
|
||||
* 禁用时文本颜色
|
||||
* Disabled text color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Disabled Text Color' })
|
||||
public disabledTextColor: number = 0xCCCCCC;
|
||||
|
||||
// ===== 动画 Animation =====
|
||||
|
||||
/**
|
||||
* 颜色过渡时长(秒)
|
||||
* Color transition duration in seconds
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Transition Duration', min: 0, step: 0.01 })
|
||||
public transitionDuration: number = 0.1;
|
||||
|
||||
/**
|
||||
* 当前显示颜色(动画插值用)
|
||||
* Current display color (for animation)
|
||||
*/
|
||||
public currentColor: number = 0x4A90D9;
|
||||
|
||||
/**
|
||||
* 目标颜色
|
||||
* Target color
|
||||
*/
|
||||
public targetColor: number = 0x4A90D9;
|
||||
|
||||
// ===== 回调 Callbacks =====
|
||||
|
||||
/**
|
||||
* 点击回调
|
||||
* Click callback
|
||||
*/
|
||||
public onClick?: () => void;
|
||||
|
||||
/**
|
||||
* 长按回调
|
||||
* Long press callback
|
||||
*/
|
||||
public onLongPress?: () => void;
|
||||
|
||||
/**
|
||||
* 长按阈值(毫秒)
|
||||
* Long press threshold in milliseconds
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Long Press Threshold', min: 0 })
|
||||
public longPressThreshold: number = 500;
|
||||
|
||||
/**
|
||||
* 长按计时器
|
||||
* Long press timer
|
||||
*/
|
||||
public pressTimer: number = 0;
|
||||
|
||||
/**
|
||||
* 是否已触发长按
|
||||
* Whether long press has been triggered
|
||||
*/
|
||||
public longPressTriggered: boolean = false;
|
||||
|
||||
// ===== 配置 Configuration =====
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
* Whether button is disabled
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Disabled' })
|
||||
public disabled: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否显示涟漪效果
|
||||
* Whether to show ripple effect
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Show Ripple' })
|
||||
public showRipple: boolean = false;
|
||||
|
||||
/**
|
||||
* 涟漪颜色
|
||||
* Ripple color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Ripple Color' })
|
||||
public rippleColor: number = 0xFFFFFF;
|
||||
|
||||
/**
|
||||
* 涟漪透明度
|
||||
* Ripple alpha
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Ripple Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public rippleAlpha: number = 0.3;
|
||||
|
||||
/**
|
||||
* 获取当前应该显示的背景颜色
|
||||
* Get the background color that should be displayed based on state
|
||||
*/
|
||||
public getStateColor(state: 'disabled' | 'pressed' | 'hovered' | 'focused' | 'normal'): number {
|
||||
if (this.disabled) return this.disabledColor;
|
||||
switch (state) {
|
||||
case 'pressed': return this.pressedColor;
|
||||
case 'hovered': return this.hoverColor;
|
||||
case 'focused': return this.focusedColor;
|
||||
default: return this.normalColor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前应该显示的纹理
|
||||
* Get the texture that should be displayed based on state
|
||||
*/
|
||||
public getStateTexture(state: 'disabled' | 'pressed' | 'hovered' | 'focused' | 'normal'): string {
|
||||
if (this.disabled && this.disabledTexture) return this.disabledTexture;
|
||||
switch (state) {
|
||||
case 'pressed': return this.pressedTexture || this.normalTexture;
|
||||
case 'hovered': return this.hoverTexture || this.normalTexture;
|
||||
case 'focused': return this.normalTexture;
|
||||
default: return this.normalTexture;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否使用纹理渲染
|
||||
* Whether to use texture for rendering
|
||||
*/
|
||||
public useTexture(): boolean {
|
||||
return (this.displayMode === 'texture' || this.displayMode === 'both') && !!this.normalTexture;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否使用颜色渲染
|
||||
* Whether to use color for rendering
|
||||
*/
|
||||
public useColor(): boolean {
|
||||
return this.displayMode === 'color' || this.displayMode === 'both';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前应该显示的文本颜色
|
||||
* Get the text color that should be displayed based on state
|
||||
*/
|
||||
public getTextColor(): number {
|
||||
return this.disabled ? this.disabledTextColor : this.textColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置颜色主题
|
||||
* Set color theme
|
||||
*/
|
||||
public setColors(normal: number, hover: number, pressed: number, disabled?: number): this {
|
||||
this.normalColor = normal;
|
||||
this.hoverColor = hover;
|
||||
this.pressedColor = pressed;
|
||||
if (disabled !== undefined) this.disabledColor = disabled;
|
||||
this.currentColor = normal;
|
||||
this.targetColor = normal;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置纹理
|
||||
* Set textures for different states
|
||||
*/
|
||||
public setTextures(normal: string, hover?: string, pressed?: string, disabled?: string): this {
|
||||
this.normalTexture = normal;
|
||||
if (hover) this.hoverTexture = hover;
|
||||
if (pressed) this.pressedTexture = pressed;
|
||||
if (disabled) this.disabledTexture = disabled;
|
||||
this.displayMode = 'texture';
|
||||
return this;
|
||||
}
|
||||
}
|
||||
337
packages/ui/src/components/widgets/UIProgressBarComponent.ts
Normal file
337
packages/ui/src/components/widgets/UIProgressBarComponent.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 进度条方向
|
||||
* Progress bar direction
|
||||
*/
|
||||
export enum UIProgressDirection {
|
||||
/** 从左到右 Left to right */
|
||||
LeftToRight = 'left-to-right',
|
||||
/** 从右到左 Right to left */
|
||||
RightToLeft = 'right-to-left',
|
||||
/** 从下到上 Bottom to top */
|
||||
BottomToTop = 'bottom-to-top',
|
||||
/** 从上到下 Top to bottom */
|
||||
TopToBottom = 'top-to-bottom'
|
||||
}
|
||||
|
||||
/**
|
||||
* 进度条填充模式
|
||||
* Progress bar fill mode
|
||||
*/
|
||||
export enum UIProgressFillMode {
|
||||
/** 水平填充 Horizontal fill */
|
||||
Horizontal = 'horizontal',
|
||||
/** 垂直填充 Vertical fill */
|
||||
Vertical = 'vertical',
|
||||
/** 圆形填充 Radial fill */
|
||||
Radial = 'radial'
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 进度条组件
|
||||
* UI ProgressBar Component - Progress indicator
|
||||
*/
|
||||
@ECSComponent('UIProgressBar')
|
||||
@Serializable({ version: 1, typeId: 'UIProgressBar' })
|
||||
export class UIProgressBarComponent extends Component {
|
||||
// ===== 数值 Values =====
|
||||
|
||||
/**
|
||||
* 当前值
|
||||
* Current value
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Value' })
|
||||
public value: number = 0;
|
||||
|
||||
/**
|
||||
* 最小值
|
||||
* Minimum value
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Min Value' })
|
||||
public minValue: number = 0;
|
||||
|
||||
/**
|
||||
* 最大值
|
||||
* Maximum value
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Max Value' })
|
||||
public maxValue: number = 100;
|
||||
|
||||
/**
|
||||
* 目标值(用于动画)
|
||||
* Target value (for animation)
|
||||
*/
|
||||
public targetValue: number = 0;
|
||||
|
||||
/**
|
||||
* 显示值(动画插值后的值)
|
||||
* Display value (interpolated for animation)
|
||||
*/
|
||||
public displayValue: number = 0;
|
||||
|
||||
// ===== 样式 Style =====
|
||||
|
||||
/**
|
||||
* 填充颜色
|
||||
* Fill color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Fill Color' })
|
||||
public fillColor: number = 0x4CAF50;
|
||||
|
||||
/**
|
||||
* 填充透明度
|
||||
* Fill alpha
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Fill Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public fillAlpha: number = 1;
|
||||
|
||||
/**
|
||||
* 背景颜色
|
||||
* Background color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Background Color' })
|
||||
public backgroundColor: number = 0x333333;
|
||||
|
||||
/**
|
||||
* 背景透明度
|
||||
* Background alpha
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Background Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public backgroundAlpha: number = 1;
|
||||
|
||||
/**
|
||||
* 边框颜色
|
||||
* Border color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Border Color' })
|
||||
public borderColor: number = 0x000000;
|
||||
|
||||
/**
|
||||
* 边框宽度
|
||||
* Border width
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Border Width', min: 0 })
|
||||
public borderWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 圆角半径
|
||||
* Corner radius
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Corner Radius', min: 0 })
|
||||
public cornerRadius: number = 0;
|
||||
|
||||
// ===== 方向和填充 Direction & Fill =====
|
||||
|
||||
/**
|
||||
* 进度方向
|
||||
* Progress direction
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Direction',
|
||||
options: [
|
||||
{ value: 'left-to-right', label: 'Left to Right' },
|
||||
{ value: 'right-to-left', label: 'Right to Left' },
|
||||
{ value: 'bottom-to-top', label: 'Bottom to Top' },
|
||||
{ value: 'top-to-bottom', label: 'Top to Bottom' }
|
||||
]
|
||||
})
|
||||
public direction: UIProgressDirection = UIProgressDirection.LeftToRight;
|
||||
|
||||
/**
|
||||
* 填充模式
|
||||
* Fill mode
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Fill Mode',
|
||||
options: [
|
||||
{ value: 'horizontal', label: 'Horizontal' },
|
||||
{ value: 'vertical', label: 'Vertical' },
|
||||
{ value: 'radial', label: 'Radial' }
|
||||
]
|
||||
})
|
||||
public fillMode: UIProgressFillMode = UIProgressFillMode.Horizontal;
|
||||
|
||||
// ===== 动画 Animation =====
|
||||
|
||||
/**
|
||||
* 过渡时长(秒)
|
||||
* Transition duration in seconds
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Transition Duration', min: 0, step: 0.01 })
|
||||
public transitionDuration: number = 0.3;
|
||||
|
||||
/**
|
||||
* 缓动函数
|
||||
* Easing function name
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'string', label: 'Easing' })
|
||||
public easing: string = 'easeOut';
|
||||
|
||||
// ===== 分段 Segments =====
|
||||
|
||||
/**
|
||||
* 是否分段显示
|
||||
* Whether to show segments
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Show Segments' })
|
||||
public showSegments: boolean = false;
|
||||
|
||||
/**
|
||||
* 分段数量
|
||||
* Number of segments
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Segments', min: 1 })
|
||||
public segments: number = 10;
|
||||
|
||||
/**
|
||||
* 分段间隙
|
||||
* Gap between segments
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Segment Gap', min: 0 })
|
||||
public segmentGap: number = 2;
|
||||
|
||||
// ===== 渐变 Gradient =====
|
||||
|
||||
/**
|
||||
* 是否使用渐变
|
||||
* Whether to use gradient fill
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Use Gradient' })
|
||||
public useGradient: boolean = false;
|
||||
|
||||
/**
|
||||
* 渐变起始颜色
|
||||
* Gradient start color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Gradient Start Color' })
|
||||
public gradientStartColor: number = 0x4CAF50;
|
||||
|
||||
/**
|
||||
* 渐变结束颜色
|
||||
* Gradient end color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Gradient End Color' })
|
||||
public gradientEndColor: number = 0x8BC34A;
|
||||
|
||||
// ===== 文本 Text =====
|
||||
|
||||
/**
|
||||
* 是否显示文本
|
||||
* Whether to show text
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Show Text' })
|
||||
public showText: boolean = false;
|
||||
|
||||
/**
|
||||
* 文本格式({value}, {percent}, {min}, {max})
|
||||
* Text format template
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'string', label: 'Text Format' })
|
||||
public textFormat: string = '{percent}%';
|
||||
|
||||
/**
|
||||
* 文本颜色
|
||||
* Text color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Text Color' })
|
||||
public textColor: number = 0xFFFFFF;
|
||||
|
||||
/**
|
||||
* 获取进度百分比 (0-1)
|
||||
* Get progress as percentage (0-1)
|
||||
*/
|
||||
public getProgress(): number {
|
||||
const range = this.maxValue - this.minValue;
|
||||
if (range <= 0) return 0;
|
||||
return Math.max(0, Math.min(1, (this.displayValue - this.minValue) / range));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取格式化的文本
|
||||
* Get formatted text
|
||||
*/
|
||||
public getFormattedText(): string {
|
||||
const percent = Math.round(this.getProgress() * 100);
|
||||
return this.textFormat
|
||||
.replace('{value}', this.displayValue.toFixed(0))
|
||||
.replace('{percent}', percent.toString())
|
||||
.replace('{min}', this.minValue.toString())
|
||||
.replace('{max}', this.maxValue.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置值(带动画)
|
||||
* Set value (with animation)
|
||||
*/
|
||||
public setValue(value: number, animate: boolean = true): this {
|
||||
this.targetValue = Math.max(this.minValue, Math.min(this.maxValue, value));
|
||||
if (!animate) {
|
||||
this.value = this.targetValue;
|
||||
this.displayValue = this.targetValue;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置颜色
|
||||
* Set colors
|
||||
*/
|
||||
public setColors(fill: number, background: number): this {
|
||||
this.fillColor = fill;
|
||||
this.backgroundColor = background;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置渐变
|
||||
* Set gradient colors
|
||||
*/
|
||||
public setGradient(startColor: number, endColor: number): this {
|
||||
this.useGradient = true;
|
||||
this.gradientStartColor = startColor;
|
||||
this.gradientEndColor = endColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加值
|
||||
* Increase value
|
||||
*/
|
||||
public increase(amount: number = 1): this {
|
||||
return this.setValue(this.targetValue + amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 减少值
|
||||
* Decrease value
|
||||
*/
|
||||
public decrease(amount: number = 1): this {
|
||||
return this.setValue(this.targetValue - amount);
|
||||
}
|
||||
}
|
||||
370
packages/ui/src/components/widgets/UIScrollViewComponent.ts
Normal file
370
packages/ui/src/components/widgets/UIScrollViewComponent.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 滚动条可见性
|
||||
* Scrollbar visibility mode
|
||||
*/
|
||||
export enum UIScrollbarVisibility {
|
||||
/** 总是显示 Always visible */
|
||||
Always = 'always',
|
||||
/** 自动显示(内容超出时)Auto show when content exceeds */
|
||||
Auto = 'auto',
|
||||
/** 总是隐藏 Always hidden */
|
||||
Hidden = 'hidden'
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 滚动视图组件
|
||||
* UI ScrollView Component - Scrollable container
|
||||
*/
|
||||
@ECSComponent('UIScrollView')
|
||||
@Serializable({ version: 1, typeId: 'UIScrollView' })
|
||||
export class UIScrollViewComponent extends Component {
|
||||
// ===== 滚动位置 Scroll Position =====
|
||||
|
||||
/**
|
||||
* 水平滚动位置
|
||||
* Horizontal scroll position
|
||||
*/
|
||||
public scrollX: number = 0;
|
||||
|
||||
/**
|
||||
* 垂直滚动位置
|
||||
* Vertical scroll position
|
||||
*/
|
||||
public scrollY: number = 0;
|
||||
|
||||
/**
|
||||
* 目标水平滚动位置(动画用)
|
||||
* Target horizontal scroll position (for animation)
|
||||
*/
|
||||
public targetScrollX: number = 0;
|
||||
|
||||
/**
|
||||
* 目标垂直滚动位置(动画用)
|
||||
* Target vertical scroll position (for animation)
|
||||
*/
|
||||
public targetScrollY: number = 0;
|
||||
|
||||
// ===== 内容尺寸 Content Size =====
|
||||
|
||||
/**
|
||||
* 内容宽度
|
||||
* Content width
|
||||
*/
|
||||
public contentWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 内容高度
|
||||
* Content height
|
||||
*/
|
||||
public contentHeight: number = 0;
|
||||
|
||||
// ===== 滚动配置 Scroll Configuration =====
|
||||
|
||||
/**
|
||||
* 是否启用水平滚动
|
||||
* Whether horizontal scroll is enabled
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Horizontal Scroll' })
|
||||
public horizontalScroll: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否启用垂直滚动
|
||||
* Whether vertical scroll is enabled
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Vertical Scroll' })
|
||||
public verticalScroll: boolean = true;
|
||||
|
||||
/**
|
||||
* 滚动条可见性
|
||||
* Scrollbar visibility mode
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Scrollbar Visibility',
|
||||
options: [
|
||||
{ value: 'always', label: 'Always' },
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
{ value: 'hidden', label: 'Hidden' }
|
||||
]
|
||||
})
|
||||
public scrollbarVisibility: UIScrollbarVisibility = UIScrollbarVisibility.Auto;
|
||||
|
||||
/**
|
||||
* 是否启用惯性滚动
|
||||
* Whether inertia scrolling is enabled
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Inertia' })
|
||||
public inertia: boolean = true;
|
||||
|
||||
/**
|
||||
* 惯性减速率
|
||||
* Inertia deceleration rate
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Deceleration Rate', min: 0, max: 1, step: 0.001 })
|
||||
public decelerationRate: number = 0.135;
|
||||
|
||||
/**
|
||||
* 是否启用弹性边界
|
||||
* Whether elastic bounds are enabled
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Elastic Bounds' })
|
||||
public elasticBounds: boolean = true;
|
||||
|
||||
/**
|
||||
* 弹性系数
|
||||
* Elasticity coefficient
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Elasticity', min: 0, max: 1, step: 0.01 })
|
||||
public elasticity: number = 0.1;
|
||||
|
||||
// ===== 滚动条样式 Scrollbar Style =====
|
||||
|
||||
/**
|
||||
* 滚动条宽度
|
||||
* Scrollbar width
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Scrollbar Width', min: 1 })
|
||||
public scrollbarWidth: number = 8;
|
||||
|
||||
/**
|
||||
* 滚动条颜色
|
||||
* Scrollbar color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Scrollbar Color' })
|
||||
public scrollbarColor: number = 0x888888;
|
||||
|
||||
/**
|
||||
* 滚动条透明度
|
||||
* Scrollbar alpha
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Scrollbar Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public scrollbarAlpha: number = 0.5;
|
||||
|
||||
/**
|
||||
* 滚动条悬停透明度
|
||||
* Scrollbar hover alpha
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Scrollbar Hover Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public scrollbarHoverAlpha: number = 0.8;
|
||||
|
||||
/**
|
||||
* 滚动条圆角
|
||||
* Scrollbar corner radius
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Scrollbar Radius', min: 0 })
|
||||
public scrollbarRadius: number = 4;
|
||||
|
||||
/**
|
||||
* 滚动条轨道颜色
|
||||
* Scrollbar track color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Scrollbar Track Color' })
|
||||
public scrollbarTrackColor: number = 0x333333;
|
||||
|
||||
/**
|
||||
* 滚动条轨道透明度
|
||||
* Scrollbar track alpha
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Scrollbar Track Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public scrollbarTrackAlpha: number = 0.3;
|
||||
|
||||
// ===== 交互状态 Interaction State =====
|
||||
|
||||
/**
|
||||
* 是否正在拖拽滚动
|
||||
* Whether currently dragging to scroll
|
||||
*/
|
||||
public dragging: boolean = false;
|
||||
|
||||
/**
|
||||
* 拖拽起始滚动位置 X
|
||||
* Drag start scroll X
|
||||
*/
|
||||
public dragStartScrollX: number = 0;
|
||||
|
||||
/**
|
||||
* 拖拽起始滚动位置 Y
|
||||
* Drag start scroll Y
|
||||
*/
|
||||
public dragStartScrollY: number = 0;
|
||||
|
||||
/**
|
||||
* 滚动速度 X(用于惯性)
|
||||
* Scroll velocity X (for inertia)
|
||||
*/
|
||||
public velocityX: number = 0;
|
||||
|
||||
/**
|
||||
* 滚动速度 Y(用于惯性)
|
||||
* Scroll velocity Y (for inertia)
|
||||
*/
|
||||
public velocityY: number = 0;
|
||||
|
||||
/**
|
||||
* 水平滚动条是否被悬停
|
||||
* Whether horizontal scrollbar is hovered
|
||||
*/
|
||||
public horizontalScrollbarHovered: boolean = false;
|
||||
|
||||
/**
|
||||
* 垂直滚动条是否被悬停
|
||||
* Whether vertical scrollbar is hovered
|
||||
*/
|
||||
public verticalScrollbarHovered: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否正在拖拽滚动条
|
||||
* Whether dragging scrollbar
|
||||
*/
|
||||
public draggingScrollbar: boolean = false;
|
||||
|
||||
// ===== 滚轮配置 Wheel Configuration =====
|
||||
|
||||
/**
|
||||
* 滚轮滚动速度
|
||||
* Mouse wheel scroll speed
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Wheel Speed', min: 1 })
|
||||
public wheelSpeed: number = 40;
|
||||
|
||||
/**
|
||||
* 是否平滑滚动
|
||||
* Whether to use smooth scrolling
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Smooth Scroll' })
|
||||
public smoothScroll: boolean = true;
|
||||
|
||||
/**
|
||||
* 平滑滚动时长(秒)
|
||||
* Smooth scroll duration in seconds
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Smooth Scroll Duration', min: 0, step: 0.01 })
|
||||
public smoothScrollDuration: number = 0.2;
|
||||
|
||||
/**
|
||||
* 获取最大水平滚动位置
|
||||
* Get maximum horizontal scroll position
|
||||
*/
|
||||
public getMaxScrollX(viewportWidth: number): number {
|
||||
return Math.max(0, this.contentWidth - viewportWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最大垂直滚动位置
|
||||
* Get maximum vertical scroll position
|
||||
*/
|
||||
public getMaxScrollY(viewportHeight: number): number {
|
||||
return Math.max(0, this.contentHeight - viewportHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置滚动位置
|
||||
* Set scroll position
|
||||
*/
|
||||
public setScroll(x: number, y: number, animate: boolean = true): this {
|
||||
this.targetScrollX = x;
|
||||
this.targetScrollY = y;
|
||||
if (!animate) {
|
||||
this.scrollX = x;
|
||||
this.scrollY = y;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到顶部
|
||||
* Scroll to top
|
||||
*/
|
||||
public scrollToTop(animate: boolean = true): this {
|
||||
return this.setScroll(this.scrollX, 0, animate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到底部
|
||||
* Scroll to bottom
|
||||
*/
|
||||
public scrollToBottom(viewportHeight: number, animate: boolean = true): this {
|
||||
return this.setScroll(this.scrollX, this.getMaxScrollY(viewportHeight), animate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到指定位置(百分比)
|
||||
* Scroll to position by percentage
|
||||
*/
|
||||
public scrollToPercent(percentX: number, percentY: number, viewportWidth: number, viewportHeight: number, animate: boolean = true): this {
|
||||
const x = this.getMaxScrollX(viewportWidth) * Math.max(0, Math.min(1, percentX));
|
||||
const y = this.getMaxScrollY(viewportHeight) * Math.max(0, Math.min(1, percentY));
|
||||
return this.setScroll(x, y, animate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要显示水平滚动条
|
||||
* Whether horizontal scrollbar should be visible
|
||||
*/
|
||||
public needsHorizontalScrollbar(viewportWidth: number): boolean {
|
||||
if (!this.horizontalScroll) return false;
|
||||
if (this.scrollbarVisibility === UIScrollbarVisibility.Hidden) return false;
|
||||
if (this.scrollbarVisibility === UIScrollbarVisibility.Always) return true;
|
||||
return this.contentWidth > viewportWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要显示垂直滚动条
|
||||
* Whether vertical scrollbar should be visible
|
||||
*/
|
||||
public needsVerticalScrollbar(viewportHeight: number): boolean {
|
||||
if (!this.verticalScroll) return false;
|
||||
if (this.scrollbarVisibility === UIScrollbarVisibility.Hidden) return false;
|
||||
if (this.scrollbarVisibility === UIScrollbarVisibility.Always) return true;
|
||||
return this.contentHeight > viewportHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取垂直滚动条手柄尺寸和位置
|
||||
* Get vertical scrollbar handle size and position
|
||||
*/
|
||||
public getVerticalScrollbarMetrics(viewportHeight: number): { size: number; position: number } {
|
||||
const maxScroll = this.getMaxScrollY(viewportHeight);
|
||||
if (maxScroll <= 0) return { size: viewportHeight, position: 0 };
|
||||
|
||||
const size = Math.max(20, (viewportHeight / this.contentHeight) * viewportHeight);
|
||||
const availableTrack = viewportHeight - size;
|
||||
const position = (this.scrollY / maxScroll) * availableTrack;
|
||||
|
||||
return { size, position };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取水平滚动条手柄尺寸和位置
|
||||
* Get horizontal scrollbar handle size and position
|
||||
*/
|
||||
public getHorizontalScrollbarMetrics(viewportWidth: number): { size: number; position: number } {
|
||||
const maxScroll = this.getMaxScrollX(viewportWidth);
|
||||
if (maxScroll <= 0) return { size: viewportWidth, position: 0 };
|
||||
|
||||
const size = Math.max(20, (viewportWidth / this.contentWidth) * viewportWidth);
|
||||
const availableTrack = viewportWidth - size;
|
||||
const position = (this.scrollX / maxScroll) * availableTrack;
|
||||
|
||||
return { size, position };
|
||||
}
|
||||
}
|
||||
390
packages/ui/src/components/widgets/UISliderComponent.ts
Normal file
390
packages/ui/src/components/widgets/UISliderComponent.ts
Normal file
@@ -0,0 +1,390 @@
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 滑块方向
|
||||
* Slider orientation
|
||||
*/
|
||||
export enum UISliderOrientation {
|
||||
Horizontal = 'horizontal',
|
||||
Vertical = 'vertical'
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 滑块组件
|
||||
* UI Slider Component - Value slider with handle
|
||||
*/
|
||||
@ECSComponent('UISlider')
|
||||
@Serializable({ version: 1, typeId: 'UISlider' })
|
||||
export class UISliderComponent extends Component {
|
||||
// ===== 数值 Values =====
|
||||
|
||||
/**
|
||||
* 当前值
|
||||
* Current value
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Value' })
|
||||
public value: number = 0;
|
||||
|
||||
/**
|
||||
* 最小值
|
||||
* Minimum value
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Min Value' })
|
||||
public minValue: number = 0;
|
||||
|
||||
/**
|
||||
* 最大值
|
||||
* Maximum value
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Max Value' })
|
||||
public maxValue: number = 100;
|
||||
|
||||
/**
|
||||
* 步进值(0 = 连续)
|
||||
* Step value (0 = continuous)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Step', min: 0 })
|
||||
public step: number = 0;
|
||||
|
||||
/**
|
||||
* 目标值(用于动画)
|
||||
* Target value (for animation)
|
||||
*/
|
||||
public targetValue: number = 0;
|
||||
|
||||
/**
|
||||
* 显示值(动画插值后)
|
||||
* Display value (interpolated)
|
||||
*/
|
||||
public displayValue: number = 0;
|
||||
|
||||
// ===== 方向 Orientation =====
|
||||
|
||||
/**
|
||||
* 滑块方向
|
||||
* Slider orientation
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Orientation',
|
||||
options: [
|
||||
{ value: 'horizontal', label: 'Horizontal' },
|
||||
{ value: 'vertical', label: 'Vertical' }
|
||||
]
|
||||
})
|
||||
public orientation: UISliderOrientation = UISliderOrientation.Horizontal;
|
||||
|
||||
// ===== 轨道样式 Track Style =====
|
||||
|
||||
/**
|
||||
* 轨道颜色
|
||||
* Track color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Track Color' })
|
||||
public trackColor: number = 0x444444;
|
||||
|
||||
/**
|
||||
* 轨道透明度
|
||||
* Track alpha
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Track Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public trackAlpha: number = 1;
|
||||
|
||||
/**
|
||||
* 轨道高度(水平)或宽度(垂直)
|
||||
* Track thickness
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Track Thickness', min: 1 })
|
||||
public trackThickness: number = 4;
|
||||
|
||||
/**
|
||||
* 轨道圆角
|
||||
* Track corner radius
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Track Radius', min: 0 })
|
||||
public trackRadius: number = 2;
|
||||
|
||||
// ===== 填充样式 Fill Style =====
|
||||
|
||||
/**
|
||||
* 填充颜色(已滑过的部分)
|
||||
* Fill color (passed portion)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Fill Color' })
|
||||
public fillColor: number = 0x4A90D9;
|
||||
|
||||
/**
|
||||
* 填充透明度
|
||||
* Fill alpha
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Fill Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public fillAlpha: number = 1;
|
||||
|
||||
// ===== 手柄样式 Handle Style =====
|
||||
|
||||
/**
|
||||
* 手柄宽度
|
||||
* Handle width
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Handle Width', min: 1 })
|
||||
public handleWidth: number = 16;
|
||||
|
||||
/**
|
||||
* 手柄高度
|
||||
* Handle height
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Handle Height', min: 1 })
|
||||
public handleHeight: number = 16;
|
||||
|
||||
/**
|
||||
* 手柄颜色
|
||||
* Handle color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Handle Color' })
|
||||
public handleColor: number = 0xFFFFFF;
|
||||
|
||||
/**
|
||||
* 手柄悬停颜色
|
||||
* Handle hover color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Handle Hover Color' })
|
||||
public handleHoverColor: number = 0xE0E0E0;
|
||||
|
||||
/**
|
||||
* 手柄按下颜色
|
||||
* Handle pressed color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Handle Pressed Color' })
|
||||
public handlePressedColor: number = 0xCCCCCC;
|
||||
|
||||
/**
|
||||
* 手柄圆角
|
||||
* Handle corner radius
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Handle Radius', min: 0 })
|
||||
public handleRadius: number = 8;
|
||||
|
||||
/**
|
||||
* 手柄边框宽度
|
||||
* Handle border width
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Handle Border Width', min: 0 })
|
||||
public handleBorderWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 手柄边框颜色
|
||||
* Handle border color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Handle Border Color' })
|
||||
public handleBorderColor: number = 0x000000;
|
||||
|
||||
/**
|
||||
* 手柄阴影
|
||||
* Handle shadow enabled
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Handle Shadow' })
|
||||
public handleShadow: boolean = true;
|
||||
|
||||
// ===== 交互状态 Interaction State =====
|
||||
|
||||
/**
|
||||
* 手柄是否被悬停
|
||||
* Whether handle is hovered
|
||||
*/
|
||||
public handleHovered: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否正在拖拽
|
||||
* Whether currently dragging
|
||||
*/
|
||||
public dragging: boolean = false;
|
||||
|
||||
/**
|
||||
* 拖拽起始值
|
||||
* Drag start value
|
||||
*/
|
||||
public dragStartValue: number = 0;
|
||||
|
||||
/**
|
||||
* 拖拽起始位置
|
||||
* Drag start position
|
||||
*/
|
||||
public dragStartPosition: number = 0;
|
||||
|
||||
// ===== 动画 Animation =====
|
||||
|
||||
/**
|
||||
* 过渡时长(秒)
|
||||
* Transition duration in seconds
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Transition Duration', min: 0, step: 0.01 })
|
||||
public transitionDuration: number = 0.1;
|
||||
|
||||
// ===== 刻度 Ticks =====
|
||||
|
||||
/**
|
||||
* 是否显示刻度
|
||||
* Whether to show ticks
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Show Ticks' })
|
||||
public showTicks: boolean = false;
|
||||
|
||||
/**
|
||||
* 刻度数量(不包括首尾)
|
||||
* Number of ticks (excluding ends)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Tick Count', min: 0 })
|
||||
public tickCount: number = 4;
|
||||
|
||||
/**
|
||||
* 刻度颜色
|
||||
* Tick color
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Tick Color' })
|
||||
public tickColor: number = 0x666666;
|
||||
|
||||
/**
|
||||
* 刻度大小
|
||||
* Tick size
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Tick Size', min: 1 })
|
||||
public tickSize: number = 4;
|
||||
|
||||
// ===== 文本 Text =====
|
||||
|
||||
/**
|
||||
* 是否显示值文本
|
||||
* Whether to show value text
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Show Value' })
|
||||
public showValue: boolean = false;
|
||||
|
||||
/**
|
||||
* 值文本格式
|
||||
* Value text format
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'string', label: 'Value Format' })
|
||||
public valueFormat: string = '{value}';
|
||||
|
||||
/**
|
||||
* 小数位数
|
||||
* Decimal places
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Decimal Places', min: 0 })
|
||||
public decimalPlaces: number = 0;
|
||||
|
||||
// ===== 回调 Callbacks =====
|
||||
|
||||
/**
|
||||
* 值改变回调
|
||||
* Value change callback
|
||||
*/
|
||||
public onChange?: (value: number) => void;
|
||||
|
||||
/**
|
||||
* 拖拽开始回调
|
||||
* Drag start callback
|
||||
*/
|
||||
public onDragStart?: (value: number) => void;
|
||||
|
||||
/**
|
||||
* 拖拽结束回调
|
||||
* Drag end callback
|
||||
*/
|
||||
public onDragEnd?: (value: number) => void;
|
||||
|
||||
/**
|
||||
* 获取进度百分比 (0-1)
|
||||
* Get progress as percentage (0-1)
|
||||
*/
|
||||
public getProgress(): number {
|
||||
const range = this.maxValue - this.minValue;
|
||||
if (range <= 0) return 0;
|
||||
return Math.max(0, Math.min(1, (this.displayValue - this.minValue) / range));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从百分比设置值
|
||||
* Set value from percentage
|
||||
*/
|
||||
public setProgress(progress: number): this {
|
||||
const range = this.maxValue - this.minValue;
|
||||
return this.setValue(this.minValue + range * Math.max(0, Math.min(1, progress)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置值
|
||||
* Set value
|
||||
*/
|
||||
public setValue(value: number, animate: boolean = true): this {
|
||||
let newValue = Math.max(this.minValue, Math.min(this.maxValue, value));
|
||||
|
||||
// 应用步进
|
||||
if (this.step > 0) {
|
||||
newValue = Math.round((newValue - this.minValue) / this.step) * this.step + this.minValue;
|
||||
}
|
||||
|
||||
this.targetValue = newValue;
|
||||
if (!animate) {
|
||||
this.value = newValue;
|
||||
this.displayValue = newValue;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取格式化的值文本
|
||||
* Get formatted value text
|
||||
*/
|
||||
public getFormattedValue(): string {
|
||||
const formattedValue = this.displayValue.toFixed(this.decimalPlaces);
|
||||
return this.valueFormat.replace('{value}', formattedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算手柄位置(归一化 0-1)
|
||||
* Calculate handle position (normalized 0-1)
|
||||
*/
|
||||
public getHandlePosition(): number {
|
||||
return this.getProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前手柄颜色
|
||||
* Get current handle color based on state
|
||||
*/
|
||||
public getCurrentHandleColor(): number {
|
||||
if (this.dragging) return this.handlePressedColor;
|
||||
if (this.handleHovered) return this.handleHoverColor;
|
||||
return this.handleColor;
|
||||
}
|
||||
}
|
||||
4
packages/ui/src/components/widgets/index.ts
Normal file
4
packages/ui/src/components/widgets/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './UIButtonComponent';
|
||||
export * from './UIProgressBarComponent';
|
||||
export * from './UISliderComponent';
|
||||
export * from './UIScrollViewComponent';
|
||||
Reference in New Issue
Block a user