Files
esengine/packages/ui/src/components/widgets/UIScrollViewComponent.ts

371 lines
10 KiB
TypeScript
Raw Normal View History

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 };
}
}