371 lines
10 KiB
TypeScript
371 lines
10 KiB
TypeScript
|
|
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 };
|
|||
|
|
}
|
|||
|
|
}
|