UNPKG

lazy-widgets

Version:

Typescript retained mode GUI for the HTML canvas API

391 lines 15 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { PointerPressEvent } from '../events/PointerPressEvent.js'; import { PointerEvent } from '../events/PointerEvent.js'; import { damageField } from '../decorators/FlagFields.js'; import { ClickHelper } from '../helpers/ClickHelper.js'; import { Widget } from './Widget.js'; import { ClickState } from '../helpers/ClickState.js'; import { KeyPressEvent } from '../events/KeyPressEvent.js'; import { FocusType } from '../core/FocusType.js'; import { KeyEvent } from '../events/KeyEvent.js'; import { Variable } from '../state/Variable.js'; import { DynMsg } from '../core/Strings.js'; import { LeaveEvent } from '../events/LeaveEvent.js'; import { PropagationModel } from '../events/WidgetEvent.js'; import { FocusEvent } from '../events/FocusEvent.js'; import { BlurEvent } from '../events/BlurEvent.js'; // TODO move minValue and maxValue to SliderProperties // TODO make vertical read-write instead of readonly // TODO make this easier to extend/customise. it's currently very hard to, for // example, modify this class in a subclass to make it look like a material // UI slider /** * A slider flexbox widget; can slide a numeric value from an inclusive minimum * value to an inclusive maximum value, with optional snapping along set * increments. * * Note that sliders can only be horizontal. * * @category Widget */ export class Slider extends Widget { constructor(variable = new Variable(0), minValue = 0, maxValue = 1, properties) { var _a, _b; // Sliders need a clear background, have no children and don't propagate // events super(properties); /** See {@link Slider#minValue} */ this._minValue = 0; /** See {@link Slider#maxValue} */ this._maxValue = 1; /** See {@link Slider#snapIncrement} */ this._snapIncrement = 0; /** The horizontal offset of the slider */ this.offsetX = 0; /** The vertical offset of the slider */ this.offsetY = 0; /** The actual width of the slider */ this.actualWidth = 0; /** The actual height of the slider */ this.actualHeight = 0; /** Is the keyboard focusing this widget? */ this.keyboardFocused = false; /** * The rectangle of the slider when the dragging started. Used to prevent * glitchy behaviour when the slider is being used while the layout is * changing. For internal use only. */ this.dragBounds = [0, 0, 0, 0]; this.clickHelper = new ClickHelper(this); this.minValue = minValue; this.maxValue = maxValue; this.snapIncrement = (_a = properties === null || properties === void 0 ? void 0 : properties.snapIncrement) !== null && _a !== void 0 ? _a : 0; this.vertical = (_b = properties === null || properties === void 0 ? void 0 : properties.vertical) !== null && _b !== void 0 ? _b : false; this.tabFocusable = true; this.variable = variable; this.callback = this.handleChange.bind(this); } handleChange() { this.markWholeAsDirty(); } handleAttachment() { this.variable.watch(this.callback); } handleDetachment() { this.variable.unwatch(this.callback); } activate() { super.activate(); this.clickHelper.reset(); } /** The slider's value */ set value(value) { this.setValue(value); } get value() { return this.variable.value; } /** * The slider's minimum value. * * Changing this does not cause the value to be clamped; clamping occurs on * value changes. */ set minValue(minValue) { if (this._minValue === minValue) { return; } if (!isFinite(minValue) || isNaN(minValue)) { throw new Error(DynMsg.INVALID_MIN(minValue)); } this._minValue = minValue; this.markWholeAsDirty(); } get minValue() { return this._minValue; } /** * The slider's maximum value. * * Changing this does not cause the value to be clamped; clamping occurs on * value changes. */ set maxValue(maxValue) { if (this._maxValue === maxValue) { return; } if (!isFinite(maxValue) || isNaN(maxValue)) { throw new Error(DynMsg.INVALID_MAX(maxValue)); } this._maxValue = maxValue; this.markWholeAsDirty(); } get maxValue() { return this._maxValue; } /** * The increments in which the slider changes value. If 0, there are no * fixed increments. * * Changing this does not cause the value to changed to match the increment; * rounding occurs on value changes. */ set snapIncrement(snapIncrement) { if (this._snapIncrement === snapIncrement) { return; } if (!isFinite(snapIncrement) || isNaN(snapIncrement)) { throw new Error(DynMsg.INVALID_INC(snapIncrement)); } if (snapIncrement < 0) { throw new Error(DynMsg.NEGATIVE_INC(snapIncrement)); } this._snapIncrement = snapIncrement; } get snapIncrement() { return this._snapIncrement; } /** Clamp a value to this slider's min and max values */ clamp(value) { if (value < this._minValue) { value = this._minValue; } else if (value > this._maxValue) { value = this._maxValue; } return value; } /** Set the slider's value, optionally using an observer group */ setValue(value, group) { // Snap to increments if needed if (this._snapIncrement > 0) { value = Math.round(value / this._snapIncrement) * this._snapIncrement; } // Update value in variable this.variable.setValue(this.clamp(value), group); } stepValue(add, incMul) { // Get snap increment. If the increment is not set, default to 1% of the // value range let effectiveIncrement = this._snapIncrement; if (effectiveIncrement === 0) { effectiveIncrement = 0.01 * (this._maxValue - this._minValue); } // Multiply increment (for holding shift) effectiveIncrement *= incMul; // Step value in increment const delta = add ? 1 : -1; this.value = this.clamp((Math.round(this.value / effectiveIncrement) + delta) * effectiveIncrement); } onThemeUpdated(property = null) { super.onThemeUpdated(property); if (property === null || property === 'sliderThickness' || property === 'sliderMinLength') { this._layoutDirty = true; this.markWholeAsDirty(); } else if (property === 'accentFill' || property === 'primaryFill' || property === 'backgroundFill') { this.markWholeAsDirty(); } } handleEvent(event) { if (event.propagation !== PropagationModel.Trickling) { if (event.isa(FocusEvent)) { if (event.focusType === FocusType.Keyboard) { this.keyboardFocused = true; } return this; } else if (event.isa(BlurEvent)) { if (event.focusType === FocusType.Keyboard) { this.keyboardFocused = false; } return this; } else { return super.handleEvent(event); } } // Ignore unhandled events if (!(event.isa(LeaveEvent) || event instanceof PointerEvent || event instanceof KeyEvent)) { return null; } // Ignore tab key presses so tab selection works, and escape so widget // unfocusing works if (event.isa(KeyPressEvent) && (event.key === 'Tab' || event.key === 'Escape')) { return null; } // Handle key presses if (event instanceof KeyEvent) { if (event.isa(KeyPressEvent)) { const incMul = event.shift ? 10 : 1; if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') { this.stepValue(false, incMul); } else if (event.key === 'ArrowRight' || event.key === 'ArrowUp') { this.stepValue(true, incMul); } } return this; } // Save slider bounds so that the slider doesn't glitch out if dragged // while the layout changes. To handle hovering properly, also update if // moving pointer, but drag hasn't been initiated if (event.isa(PointerPressEvent) || this.clickHelper.clickState !== ClickState.Hold) { const x = this.idealX + this.offsetX; const y = this.idealY + this.offsetY; this.dragBounds[0] = x; this.dragBounds[1] = x + this.actualWidth; this.dragBounds[2] = y; this.dragBounds[3] = y + this.actualHeight; } // Handle click event this.clickHelper.handleClickEvent(event, this.root, this.dragBounds); // If this was a click or the slider is currently being held, update // value if (((this.clickHelper.clickStateChanged && this.clickHelper.wasClick) || this.clickHelper.clickState === ClickState.Hold) && this.clickHelper.pointerPos !== null) { // Interpolate value const percent = this.vertical ? (1 - this.clickHelper.pointerPos[1]) : this.clickHelper.pointerPos[0]; this.value = this._minValue + percent * (this._maxValue - this._minValue); } // Always flag as dirty if the click state changed (so glow colour takes // effect) if (this.clickHelper.clickStateChanged) { this.markWholeAsDirty(); } return this; } handleResolveDimensions(minWidth, maxWidth, minHeight, maxHeight) { // Get theme properties const thickness = this.sliderThickness; const minLength = this.sliderMinLength; // Fully expand along main axis if constrained and center along cross // axis if (this.vertical) { // Main axis if (maxHeight != Infinity) { this.idealHeight = maxHeight; } else { this.idealHeight = Math.max(minLength, minHeight); } // Cross axis this.idealWidth = Math.min(Math.max(thickness, minWidth), maxWidth); } else { // Main axis if (maxWidth != Infinity) { this.idealWidth = maxWidth; } else { this.idealWidth = Math.max(minLength, minWidth); } // Cross axis this.idealHeight = Math.min(Math.max(thickness, minHeight), maxHeight); } } finalizeBounds() { super.finalizeBounds(); // cache centered position and dimensions if (this.vertical) { this.actualWidth = Math.min(this.width, this.sliderThickness); this.actualHeight = this.height; this.offsetX = (this.width - this.actualWidth) / 2; this.offsetY = 0; } else { this.actualWidth = this.width; this.actualHeight = Math.min(this.height, this.sliderThickness); this.offsetX = 0; this.offsetY = (this.height - this.actualHeight) / 2; } } handlePainting(_dirtyRects) { // Correct position with offset const x = this.x + this.offsetX; const y = this.y + this.offsetY; // Setup style for filled part of slider. Use accent colour if hovering // or holding const ctx = this.viewport.context; const useGlow = this.keyboardFocused || this.clickHelper.clickState === ClickState.Hover || this.clickHelper.clickState === ClickState.Hold; if (useGlow) { ctx.fillStyle = this.accentFill; } else { ctx.fillStyle = this.primaryFill; } if (this.vertical) { // bottom-to-top // Draw full part of slider const fullHeight = this.actualHeight * Math.max(0, Math.min(1, (this.value - this._minValue) / (this._maxValue - this._minValue))); ctx.fillRect(x, y + this.actualHeight - fullHeight, this.actualWidth, fullHeight); // Draw empty part of slider const emptyHeight = this.actualHeight - fullHeight; if (emptyHeight > 0) { if (useGlow) { ctx.fillStyle = this.backgroundGlowFill; } else { ctx.fillStyle = this.backgroundFill; } ctx.fillRect(x, y, this.actualWidth, emptyHeight); } } else { // left-to-right // Draw full part of slider const fullWidth = this.actualWidth * Math.max(0, Math.min(1, (this.value - this._minValue) / (this._maxValue - this._minValue))); ctx.fillRect(x, y, fullWidth, this.actualHeight); // Draw empty part of slider const emptyWidth = this.actualWidth - fullWidth; if (emptyWidth > 0) { if (useGlow) { ctx.fillStyle = this.backgroundGlowFill; } else { ctx.fillStyle = this.backgroundFill; } ctx.fillRect(x + fullWidth, y, emptyWidth, this.actualHeight); } } } handlePreLayoutUpdate() { super.handlePreLayoutUpdate(); this.clickHelper.doneProcessing(); } } Slider.autoXML = { name: 'slider', inputConfig: [ { mode: 'value', name: 'variable', validator: 'box', optional: true }, { mode: 'value', name: 'min-value', validator: 'number', optional: true }, { mode: 'value', name: 'max-value', validator: 'number', optional: true } ] }; __decorate([ damageField ], Slider.prototype, "keyboardFocused", void 0); //# sourceMappingURL=Slider.js.map