UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

310 lines (307 loc) 10.6 kB
import { math } from '../../../core/math/math.js'; import { ORIENTATION_HORIZONTAL } from '../../../scene/constants.js'; import { GraphNode } from '../../../scene/graph-node.js'; import { Component } from '../component.js'; import { ElementDragHelper } from '../element/element-drag-helper.js'; /** * @import { EventHandle } from '../../../core/event-handle.js' * @import { Entity } from '../../entity.js' * @import { ScrollbarComponentData } from './data.js' * @import { ScrollbarComponentSystem } from './system.js' */ /** * A ScrollbarComponent enables a group of entities to behave like a draggable scrollbar. * * @hideconstructor * @category User Interface */ class ScrollbarComponent extends Component { static{ /** * Fired whenever the scroll value changes. The handler is passed a number representing the * current scroll value. * * @event * @example * entity.scrollbar.on('set:value', (value) => { * console.log(`Scroll value is now ${value}`); * }); */ this.EVENT_SETVALUE = 'set:value'; } /** * Create a new ScrollbarComponent. * * @param {ScrollbarComponentSystem} system - The ComponentSystem that created this Component. * @param {Entity} entity - The Entity that this Component is attached to. */ constructor(system, entity){ super(system, entity), /** * @type {Entity|null} * @private */ this._handleEntity = null, /** * @type {EventHandle|null} * @private */ this._evtHandleEntityElementAdd = null, /** * @type {EventHandle[]} * @private */ this._evtHandleEntityChanges = []; this._toggleLifecycleListeners('on'); } // TODO: Remove this override in upgrading component /** * @type {ScrollbarComponentData} * @ignore */ get data() { const record = this.system.store[this.entity.getGuid()]; return record ? record.data : null; } /** * Sets the enabled state of the component. * * @type {boolean} */ set enabled(arg) { this._setValue('enabled', arg); } /** * Gets the enabled state of the component. * * @type {boolean} */ get enabled() { return this.data.enabled; } /** * Sets whether the scrollbar moves horizontally or vertically. Can be: * * - {@link ORIENTATION_HORIZONTAL}: The scrollbar animates in the horizontal axis. * - {@link ORIENTATION_VERTICAL}: The scrollbar animates in the vertical axis. * * Defaults to {@link ORIENTATION_HORIZONTAL}. * * @type {number} */ set orientation(arg) { this._setValue('orientation', arg); } /** * Gets whether the scrollbar moves horizontally or vertically. * * @type {number} */ get orientation() { return this.data.orientation; } /** * Sets the current position value of the scrollbar, in the range 0 to 1. Defaults to 0. * * @type {number} */ set value(arg) { this._setValue('value', arg); } /** * Gets the current position value of the scrollbar. * * @type {number} */ get value() { return this.data.value; } /** * Sets the size of the handle relative to the size of the track, in the range 0 to 1. For a * vertical scrollbar, a value of 1 means that the handle will take up the full height of the * track. * * @type {number} */ set handleSize(arg) { this._setValue('handleSize', arg); } /** * Gets the size of the handle relative to the size of the track. * * @type {number} */ get handleSize() { return this.data.handleSize; } /** * Sets the entity to be used as the scrollbar handle. This entity must have a * {@link ScrollbarComponent}. * * @type {Entity|string|null} */ set handleEntity(arg) { if (this._handleEntity === arg) { return; } const isString = typeof arg === 'string'; if (this._handleEntity && isString && this._handleEntity.getGuid() === arg) { return; } if (this._handleEntity) { this._handleEntityUnsubscribe(); } if (arg instanceof GraphNode) { this._handleEntity = arg; } else if (isString) { this._handleEntity = this.system.app.getEntityFromIndex(arg) || null; } else { this._handleEntity = null; } if (this._handleEntity) { this._handleEntitySubscribe(); } if (this._handleEntity) { this.data.handleEntity = this._handleEntity.getGuid(); } else if (isString && arg) { this.data.handleEntity = arg; } } /** * Gets the entity to be used as the scrollbar handle. * * @type {Entity|null} */ get handleEntity() { return this._handleEntity; } /** @ignore */ _setValue(name, value) { const data = this.data; const oldValue = data[name]; data[name] = value; this.fire('set', name, oldValue, value); } /** * @param {string} onOrOff - 'on' or 'off'. * @private */ _toggleLifecycleListeners(onOrOff) { this[onOrOff]('set_value', this._onSetValue, this); this[onOrOff]('set_handleSize', this._onSetHandleSize, this); this[onOrOff]('set_orientation', this._onSetOrientation, this); // TODO Handle scrollwheel events } _handleEntitySubscribe() { this._evtHandleEntityElementAdd = this._handleEntity.on('element:add', this._onHandleElementGain, this); if (this._handleEntity.element) { this._onHandleElementGain(); } } _handleEntityUnsubscribe() { this._evtHandleEntityElementAdd?.off(); this._evtHandleEntityElementAdd = null; if (this._handleEntity?.element) { this._onHandleElementLose(); } } _handleEntityElementSubscribe() { const element = this._handleEntity.element; const handles = this._evtHandleEntityChanges; handles.push(element.once('beforeremove', this._onHandleElementLose, this)); handles.push(element.on('set:anchor', this._onSetHandleAlignment, this)); handles.push(element.on('set:margin', this._onSetHandleAlignment, this)); handles.push(element.on('set:pivot', this._onSetHandleAlignment, this)); } _handleEntityElementUnsubscribe() { for(let i = 0; i < this._evtHandleEntityChanges.length; i++){ this._evtHandleEntityChanges[i].off(); } this._evtHandleEntityChanges.length = 0; } _onHandleElementGain() { this._handleEntityElementSubscribe(); this._destroyDragHelper(); this._handleDragHelper = new ElementDragHelper(this._handleEntity.element, this._getAxis()); this._handleDragHelper.on('drag:move', this._onHandleDrag, this); this._updateHandlePositionAndSize(); } _onHandleElementLose() { this._handleEntityElementUnsubscribe(); this._destroyDragHelper(); } _onHandleDrag(position) { if (this._handleEntity && this.enabled && this.entity.enabled) { this.value = this._handlePositionToScrollValue(position[this._getAxis()]); } } _onSetValue(name, oldValue, newValue) { if (Math.abs(newValue - oldValue) > 1e-5) { this.data.value = math.clamp(newValue, 0, 1); this._updateHandlePositionAndSize(); this.fire('set:value', this.data.value); } } _onSetHandleSize(name, oldValue, newValue) { if (Math.abs(newValue - oldValue) > 1e-5) { this.data.handleSize = math.clamp(newValue, 0, 1); this._updateHandlePositionAndSize(); } } _onSetHandleAlignment() { this._updateHandlePositionAndSize(); } _onSetOrientation(name, oldValue, newValue) { if (newValue !== oldValue && this._handleEntity?.element) { this._handleEntity.element[this._getOppositeDimension()] = 0; } } _updateHandlePositionAndSize() { const handleEntity = this._handleEntity; const handleElement = handleEntity?.element; if (handleEntity) { const position = handleEntity.getLocalPosition(); position[this._getAxis()] = this._getHandlePosition(); handleEntity.setLocalPosition(position); } if (handleElement) { handleElement[this._getDimension()] = this._getHandleLength(); } } _handlePositionToScrollValue(handlePosition) { return handlePosition * this._getSign() / this._getUsableTrackLength(); } _scrollValueToHandlePosition(value) { return value * this._getSign() * this._getUsableTrackLength(); } _getUsableTrackLength() { return Math.max(this._getTrackLength() - this._getHandleLength(), 0.001); } _getTrackLength() { if (this.entity.element) { return this.orientation === ORIENTATION_HORIZONTAL ? this.entity.element.calculatedWidth : this.entity.element.calculatedHeight; } return 0; } _getHandleLength() { return this._getTrackLength() * this.handleSize; } _getHandlePosition() { return this._scrollValueToHandlePosition(this.value); } _getSign() { return this.orientation === ORIENTATION_HORIZONTAL ? 1 : -1; } _getAxis() { return this.orientation === ORIENTATION_HORIZONTAL ? 'x' : 'y'; } _getDimension() { return this.orientation === ORIENTATION_HORIZONTAL ? 'width' : 'height'; } _getOppositeDimension() { return this.orientation === ORIENTATION_HORIZONTAL ? 'height' : 'width'; } _destroyDragHelper() { if (this._handleDragHelper) { this._handleDragHelper.destroy(); } } _setHandleDraggingEnabled(enabled) { if (this._handleDragHelper) { this._handleDragHelper.enabled = enabled; } } onEnable() { this._setHandleDraggingEnabled(true); } onDisable() { this._setHandleDraggingEnabled(false); } onRemove() { this._destroyDragHelper(); this._toggleLifecycleListeners('off'); } resolveDuplicatedEntityReferenceProperties(oldScrollbar, duplicatedIdsMap) { if (oldScrollbar.handleEntity) { this.handleEntity = duplicatedIdsMap[oldScrollbar.handleEntity.getGuid()]; } } } export { ScrollbarComponent };