@@ -3,6 +3,8 @@ import { UITransformComponent } from '../components/UITransformComponent';
import { UIInteractableComponent } from '../components/UIInteractableComponent' ;
import { UIButtonComponent } from '../components/widgets/UIButtonComponent' ;
import { UISliderComponent } from '../components/widgets/UISliderComponent' ;
import { UIScrollViewComponent } from '../components/widgets/UIScrollViewComponent' ;
import type { UILayoutSystem } from './UILayoutSystem' ;
/**
* 鼠标按钮
@@ -58,6 +60,23 @@ export class UIInputSystem extends EntitySystem {
private lastClickEntity : Entity | null = null ;
private doubleClickThreshold : number = 300 ; // ms
// ===== 滚轮状态 Wheel State =====
private wheelDeltaX : number = 0 ;
private wheelDeltaY : number = 0 ;
private hasWheelEvent : boolean = false ;
// ===== ScrollView 拖拽状态 ScrollView Drag State =====
private scrollViewDragTarget : Entity | null = null ;
private scrollViewDragStartX : number = 0 ;
private scrollViewDragStartY : number = 0 ;
private scrollViewDragStartScrollX : number = 0 ;
private scrollViewDragStartScrollY : number = 0 ;
private scrollViewLastDragX : number = 0 ;
private scrollViewLastDragY : number = 0 ;
private scrollViewLastDragTime : number = 0 ;
// ===== 事件监听器 Event Listeners =====
private canvas : HTMLCanvasElement | null = null ;
@@ -66,6 +85,11 @@ export class UIInputSystem extends EntitySystem {
private boundMouseUp : ( e : MouseEvent ) = > void ;
private boundWheel : ( e : WheelEvent ) = > void ;
// ===== UI 布局系统引用 UI Layout System Reference =====
// 用于获取 UI 画布尺寸以进行坐标转换
// Used to get UI canvas size for coordinate conversion
private layoutSystem : UILayoutSystem | null = null ;
constructor ( ) {
super ( Matcher . empty ( ) . all ( UITransformComponent , UIInteractableComponent ) ) ;
@@ -75,6 +99,17 @@ export class UIInputSystem extends EntitySystem {
this . boundWheel = this . onWheel . bind ( this ) ;
}
/**
* 设置 UI 布局系统引用
* Set UI layout system reference
*
* 用于获取 UI 画布尺寸以进行坐标转换
* Used to get UI canvas size for coordinate conversion
*/
public setLayoutSystem ( layoutSystem : UILayoutSystem ) : void {
this . layoutSystem = layoutSystem ;
}
/**
* 绑定到 Canvas 元素
* Bind to canvas element
@@ -128,7 +163,30 @@ export class UIInputSystem extends EntitySystem {
private onMouseMove ( e : MouseEvent ) : void {
const rect = this . canvas ! . getBoundingClientRect ( ) ;
this . setMousePosition ( e . clientX - rect . left , e . clientY - rect . top ) ;
// 屏幕坐标( CSS 像素,左上角为 0,0, Y 向下)
const screenX = e . clientX - rect . left ;
const screenY = e . clientY - rect . top ;
// 获取 UI 画布尺寸
const uiCanvasSize = this . layoutSystem ? . getCanvasSize ( ) ? ? { width : 1920 , height : 1080 } ;
// 转换为 UI 世界坐标(中心为 0,0, Y 向上)
// UI 坐标系:左上角是 (-width/2, +height/2),右下角是 (+width/2, -height/2)
// 屏幕坐标系:左上角是 (0, 0),右下角是 (canvasWidth, canvasHeight)
const canvasWidth = rect . width ;
const canvasHeight = rect . height ;
// 屏幕坐标归一化到 [0, 1]
const normalizedX = screenX / canvasWidth ;
const normalizedY = screenY / canvasHeight ;
// 转换为 UI 世界坐标
// X: 0 -> -uiCanvasWidth/2, 1 -> +uiCanvasWidth/2
// Y: 0 -> +uiCanvasHeight/2 (顶部), 1 -> -uiCanvasHeight/2 (底部)
const worldX = ( normalizedX - 0.5 ) * uiCanvasSize . width ;
const worldY = ( 0.5 - normalizedY ) * uiCanvasSize . height ;
this . setMousePosition ( worldX , worldY ) ;
}
private onMouseDown ( e : MouseEvent ) : void {
@@ -139,11 +197,17 @@ export class UIInputSystem extends EntitySystem {
this . setMouseButton ( e . button as MouseButton , false ) ;
}
private onWheel ( _ e : WheelEvent ) : void {
// TODO: 处理滚轮事件
private onWheel ( e : WheelEvent ) : void {
e . preventDefault ( ) ;
this . wheelDeltaX = e . deltaX ;
this . wheelDeltaY = e . deltaY ;
this . hasWheelEvent = true ;
}
protected process ( entities : readonly Entity [ ] ) : void {
// 如果没有绑定 canvas, 不处理输入
if ( ! this . canvas ) return ;
const dt = Time . deltaTime ;
// 按 zIndex 从高到低排序,确保上层元素优先处理
@@ -214,6 +278,7 @@ export class UIInputSystem extends EntitySystem {
// 处理特殊控件
this . handleSlider ( entity ) ;
this . handleButton ( entity , interactable ) ;
this . handleScrollView ( entity , transform ) ;
// 阻止事件传递到下层
if ( interactable . blockEvents ) {
@@ -230,6 +295,9 @@ export class UIInputSystem extends EntitySystem {
if ( interactable . pressed && ! this . mouseButtons [ MouseButton . Left ] ) {
interactable . pressed = false ;
}
// 即使鼠标不在元素上,也需要更新按钮的目标颜色(恢复到 normal)
this . handleButton ( entity , interactable ) ;
}
}
@@ -291,6 +359,13 @@ export class UIInputSystem extends EntitySystem {
this . lastClickTime = 0 ;
} else {
interactable . onClick ? . ( ) ;
// 如果是按钮,也调用按钮的 onClick
const button = entity . getComponent ( UIButtonComponent ) ;
if ( button && ! button . disabled ) {
button . onClick ? . ( ) ;
}
this . lastClickEntity = entity ;
this . lastClickTime = now ;
}
@@ -344,8 +419,11 @@ export class UIInputSystem extends EntitySystem {
const button = entity . getComponent ( UIButtonComponent ) ;
if ( ! button || button . disabled ) return ;
// 更新目标颜色
button . target Color = button . getStateColor ( interactable . getState ( ) ) ;
// 更新目标颜色和当前颜色
const state Color = button . getStateColor ( interactable . getState ( ) ) ;
button . targetColor = stateColor ;
// 直接设置 currentColor 以便立即看到效果(动画系统会平滑过渡)
button . currentColor = stateColor ;
// 处理长按
if ( interactable . pressed ) {
@@ -365,6 +443,220 @@ export class UIInputSystem extends EntitySystem {
}
}
private handleScrollView ( entity : Entity , transform : UITransformComponent ) : void {
const scrollView = entity . getComponent ( UIScrollViewComponent ) ;
if ( ! scrollView ) return ;
const viewportWidth = transform . computedWidth ;
const viewportHeight = transform . computedHeight ;
const maxScrollX = scrollView . getMaxScrollX ( viewportWidth ) ;
const maxScrollY = scrollView . getMaxScrollY ( viewportHeight ) ;
// 处理滚轮事件
if ( this . hasWheelEvent ) {
let deltaX = 0 ;
let deltaY = 0 ;
if ( scrollView . verticalScroll && maxScrollY > 0 ) {
deltaY = this . wheelDeltaY * ( scrollView . wheelSpeed / 40 ) ;
}
if ( scrollView . horizontalScroll && maxScrollX > 0 ) {
deltaX = this . wheelDeltaX * ( scrollView . wheelSpeed / 40 ) ;
}
if ( deltaX !== 0 || deltaY !== 0 ) {
if ( scrollView . smoothScroll ) {
scrollView . targetScrollX = this . clampScroll ( scrollView . targetScrollX + deltaX , maxScrollX ) ;
scrollView . targetScrollY = this . clampScroll ( scrollView . targetScrollY + deltaY , maxScrollY ) ;
} else {
scrollView . scrollX = this . clampScroll ( scrollView . scrollX + deltaX , maxScrollX ) ;
scrollView . scrollY = this . clampScroll ( scrollView . scrollY + deltaY , maxScrollY ) ;
scrollView . targetScrollX = scrollView . scrollX ;
scrollView . targetScrollY = scrollView . scrollY ;
}
scrollView . velocityX = 0 ;
scrollView . velocityY = 0 ;
}
this . hasWheelEvent = false ;
this . wheelDeltaX = 0 ;
this . wheelDeltaY = 0 ;
}
// 处理内容拖拽滚动
const isLeftPressed = this . mouseButtons [ MouseButton . Left ] ! ;
const wasLeftPressed = this . prevMouseButtons [ MouseButton . Left ] ! ;
if ( isLeftPressed && ! wasLeftPressed && ! scrollView . dragging ) {
// 开始拖拽
scrollView . dragging = true ;
scrollView . dragStartScrollX = scrollView . scrollX ;
scrollView . dragStartScrollY = scrollView . scrollY ;
this . scrollViewDragTarget = entity ;
this . scrollViewDragStartX = this . mouseX ;
this . scrollViewDragStartY = this . mouseY ;
this . scrollViewDragStartScrollX = scrollView . scrollX ;
this . scrollViewDragStartScrollY = scrollView . scrollY ;
this . scrollViewLastDragX = this . mouseX ;
this . scrollViewLastDragY = this . mouseY ;
this . scrollViewLastDragTime = performance . now ( ) ;
scrollView . velocityX = 0 ;
scrollView . velocityY = 0 ;
}
if ( scrollView . dragging && this . scrollViewDragTarget === entity ) {
if ( isLeftPressed ) {
// 拖拽中
const dragDeltaX = this . mouseX - this . scrollViewDragStartX ;
const dragDeltaY = this . mouseY - this . scrollViewDragStartY ;
let newScrollX = this . scrollViewDragStartScrollX - dragDeltaX ;
let newScrollY = this . scrollViewDragStartScrollY - dragDeltaY ;
if ( scrollView . horizontalScroll ) {
if ( scrollView . elasticBounds ) {
newScrollX = this . applyElasticBounds ( newScrollX , maxScrollX , scrollView . elasticity ) ;
} else {
newScrollX = this . clampScroll ( newScrollX , maxScrollX ) ;
}
scrollView . scrollX = newScrollX ;
scrollView . targetScrollX = newScrollX ;
}
if ( scrollView . verticalScroll ) {
if ( scrollView . elasticBounds ) {
newScrollY = this . applyElasticBounds ( newScrollY , maxScrollY , scrollView . elasticity ) ;
} else {
newScrollY = this . clampScroll ( newScrollY , maxScrollY ) ;
}
scrollView . scrollY = newScrollY ;
scrollView . targetScrollY = newScrollY ;
}
// 计算速度(用于惯性)
const now = performance . now ( ) ;
const timeDelta = ( now - this . scrollViewLastDragTime ) / 1000 ;
if ( timeDelta > 0 ) {
scrollView . velocityX = ( this . scrollViewLastDragX - this . mouseX ) / timeDelta ;
scrollView . velocityY = ( this . scrollViewLastDragY - this . mouseY ) / timeDelta ;
}
this . scrollViewLastDragX = this . mouseX ;
this . scrollViewLastDragY = this . mouseY ;
this . scrollViewLastDragTime = now ;
} else {
// 结束拖拽
scrollView . dragging = false ;
this . scrollViewDragTarget = null ;
// 应用惯性
if ( ! scrollView . inertia ) {
scrollView . velocityX = 0 ;
scrollView . velocityY = 0 ;
}
}
}
// 更新惯性滚动
this . updateScrollViewInertia ( scrollView , maxScrollX , maxScrollY ) ;
// 平滑滚动到目标位置
this . updateScrollViewSmooth ( scrollView ) ;
}
private clampScroll ( value : number , max : number ) : number {
return Math . max ( 0 , Math . min ( max , value ) ) ;
}
private applyElasticBounds ( value : number , max : number , elasticity : number ) : number {
if ( value < 0 ) {
return value * elasticity ;
}
if ( value > max ) {
return max + ( value - max ) * elasticity ;
}
return value ;
}
private updateScrollViewInertia ( scrollView : UIScrollViewComponent , maxScrollX : number , maxScrollY : number ) : void {
if ( scrollView . dragging ) return ;
const dt = Time . deltaTime ;
const deceleration = scrollView . decelerationRate ;
const minVelocity = 10 ;
// 应用减速
if ( Math . abs ( scrollView . velocityX ) > minVelocity || Math . abs ( scrollView . velocityY ) > minVelocity ) {
scrollView . velocityX *= Math . pow ( deceleration , dt * 60 ) ;
scrollView . velocityY *= Math . pow ( deceleration , dt * 60 ) ;
// 更新滚动位置
if ( scrollView . horizontalScroll ) {
scrollView . scrollX += scrollView . velocityX * dt ;
scrollView . targetScrollX = scrollView . scrollX ;
}
if ( scrollView . verticalScroll ) {
scrollView . scrollY += scrollView . velocityY * dt ;
scrollView . targetScrollY = scrollView . scrollY ;
}
// 停止条件
if ( Math . abs ( scrollView . velocityX ) < minVelocity ) scrollView . velocityX = 0 ;
if ( Math . abs ( scrollView . velocityY ) < minVelocity ) scrollView . velocityY = 0 ;
}
// 弹回边界
if ( scrollView . elasticBounds && ! scrollView . dragging ) {
const bounceSpeed = 8 ;
if ( scrollView . scrollX < 0 ) {
scrollView . scrollX += ( 0 - scrollView . scrollX ) * bounceSpeed * dt ;
scrollView . targetScrollX = scrollView . scrollX ;
if ( Math . abs ( scrollView . scrollX ) < 0.5 ) scrollView . scrollX = 0 ;
} else if ( scrollView . scrollX > maxScrollX ) {
scrollView . scrollX += ( maxScrollX - scrollView . scrollX ) * bounceSpeed * dt ;
scrollView . targetScrollX = scrollView . scrollX ;
if ( Math . abs ( scrollView . scrollX - maxScrollX ) < 0.5 ) scrollView . scrollX = maxScrollX ;
}
if ( scrollView . scrollY < 0 ) {
scrollView . scrollY += ( 0 - scrollView . scrollY ) * bounceSpeed * dt ;
scrollView . targetScrollY = scrollView . scrollY ;
if ( Math . abs ( scrollView . scrollY ) < 0.5 ) scrollView . scrollY = 0 ;
} else if ( scrollView . scrollY > maxScrollY ) {
scrollView . scrollY += ( maxScrollY - scrollView . scrollY ) * bounceSpeed * dt ;
scrollView . targetScrollY = scrollView . scrollY ;
if ( Math . abs ( scrollView . scrollY - maxScrollY ) < 0.5 ) scrollView . scrollY = maxScrollY ;
}
} else {
// 硬边界限制
scrollView . scrollX = this . clampScroll ( scrollView . scrollX , maxScrollX ) ;
scrollView . scrollY = this . clampScroll ( scrollView . scrollY , maxScrollY ) ;
}
}
private updateScrollViewSmooth ( scrollView : UIScrollViewComponent ) : void {
if ( scrollView . dragging ) return ;
if ( scrollView . velocityX !== 0 || scrollView . velocityY !== 0 ) return ;
const dt = Time . deltaTime ;
const smoothSpeed = 1 / Math . max ( 0.01 , scrollView . smoothScrollDuration ) ;
const diffX = scrollView . targetScrollX - scrollView . scrollX ;
const diffY = scrollView . targetScrollY - scrollView . scrollY ;
if ( Math . abs ( diffX ) > 0.5 ) {
scrollView . scrollX += diffX * smoothSpeed * dt ;
} else {
scrollView . scrollX = scrollView . targetScrollX ;
}
if ( Math . abs ( diffY ) > 0.5 ) {
scrollView . scrollY += diffY * smoothSpeed * dt ;
} else {
scrollView . scrollY = scrollView . targetScrollY ;
}
}
private updateCursor ( hoveredEntity : Entity | null ) : void {
if ( ! this . canvas ) return ;