UNPKG

lazy-widgets

Version:

Typescript retained mode GUI for the HTML canvas API

292 lines 13.2 kB
import { ClippedViewport } from '../core/ClippedViewport.js'; import { CanvasViewport } from '../core/CanvasViewport.js'; import { AxisCoupling } from '../core/AxisCoupling.js'; import { Widget } from './Widget.js'; import { SingleParent } from './SingleParent.js'; import { DynMsg } from '../core/Strings.js'; import { PropagationModel } from '../events/WidgetEvent.js'; import { SingleParentXMLInputConfig } from '../xml/SingleParentXMLInputConfig.js'; import { ScrollEvent } from '../events/ScrollEvent.js'; import { viewportRelativePointToAbsolute } from '../helpers/viewportRelativePointToAbsolute.js'; import { viewportRelativeRectToAbsolute } from '../helpers/viewportRelativeRectToAbsolute.js'; import { clipRelativeRectToAbsoluteViewport } from '../helpers/clipRelativeRectToAbsoluteViewport.js'; // TODO finalizeBounds is called multiple times. this has no side-effects other // than being less efficient. note that finalizeBounds must be able to be called // multiple times, not just once per frame. this is because of canvas scaling // triggering a need for re-rounding dimensions and positions /** * A type of container widget which is allowed to be bigger or smaller than its * child. * * Can be constrained to a specific type of children. * * Allows setting the offset of the child, automatically clips it if neccessary. * Otherwise acts like a {@link Container}. Implemented by force re-painting the * child and clipping it or, optionally, by using a {@link Viewport} to paint * the child widget to a dedicated canvas. * * Note that, if using a {@link CanvasViewport} by setting * {@link ViewportWidget#useCanvas} to true, widgets may be blurry when the * offset is not aligned to the same grid as the parent viewport. This is not * fixable. For better quality use {@link ClippedViewport}; only use * {@link ViewportWidget#useCanvas} when absolutely necessary. * * @category Widget */ export class ViewportWidget extends SingleParent { constructor(child, properties) { var _a, _b, _c, _d; // Viewport clears its own background, has a single child and propagates // events super(child, properties); /** {@link ViewportWidget#_constraints} but for internal use. */ this._constraints = [0, Infinity, 0, Infinity]; /** * The amount of horizontal space to reserve. By default, no space is * reserved. Useful for situations where additional parts are needed around * the viewport, such as scrollbars in {@link ScrollableViewportWidget}. * * Note that if scaling is being used, then these values are expected to * already be scaled. * * Should be set before {@link ViewportWidget#handleResolveDimensions} is * called. */ this.reservedX = 0; /** Similar to {@link ViewportWidget#reservedX}, but vertical. */ this.reservedY = 0; this.useCanvas = (_a = properties === null || properties === void 0 ? void 0 : properties.useCanvas) !== null && _a !== void 0 ? _a : false; if (this.useCanvas) { this.internalViewport = new CanvasViewport(child); } else { this.internalViewport = new ClippedViewport(child); } this._widthCoupling = (_b = properties === null || properties === void 0 ? void 0 : properties.widthCoupling) !== null && _b !== void 0 ? _b : AxisCoupling.None; this._heightCoupling = (_c = properties === null || properties === void 0 ? void 0 : properties.heightCoupling) !== null && _c !== void 0 ? _c : AxisCoupling.None; this._constraints = (_d = properties === null || properties === void 0 ? void 0 : properties.constraints) !== null && _d !== void 0 ? _d : [0, Infinity, 0, Infinity]; } /** * Offset of {@link SingleParent#child}. Positional events will take this * into account, as well as rendering. Useful for implementing scrolling. */ get offset() { return [...this.internalViewport.offset]; } set offset(offset) { // Not using @damageArrayField so that accessor can be overridden const [oldX, oldY] = this.internalViewport.offset; if (oldX !== offset[0] || oldY !== offset[1]) { this.internalViewport.offset = [offset[0], offset[1]]; this.markWholeAsDirty(); const [newX, newY] = this.internalViewport.offset; const deltaX = newX - oldX; const deltaY = newY - oldY; if (deltaX !== 0 || deltaY !== 0) { this.dispatchEvent(new ScrollEvent(this, newX, newY, deltaX, deltaY)); } } } /** * Child constraints for resolving layout. May be different than * {@link ViewportWidget#internalViewport}'s constraints. By default, this * is 0 minimum and Infinity maximum per axis. * * Will be automatically scaled depending on the current {@link Root}'s * resolution. * * Will also update the constraints of the * {@link ViewportWidget#internalViewport | Viewport}, but may be different * due to {@link ViewportWidget#widthCoupling} or * {@link ViewportWidget#heightCoupling}. */ set constraints(constraints) { if (constraints[0] !== this._constraints[0] || constraints[1] !== this._constraints[1] || constraints[2] !== this._constraints[2] || constraints[3] !== this._constraints[3]) { // Update own constraints this._constraints[0] = constraints[0]; this._constraints[1] = constraints[1]; this._constraints[2] = constraints[2]; this._constraints[3] = constraints[3]; // Update viewport's constaints this.internalViewport.constraints = constraints; } } get constraints() { return [...this._constraints]; } /** * Is the width coupled to the child's? If not {@link AxisCoupling.None}, * width constraints will be ignored or augmented. */ get widthCoupling() { return this._widthCoupling; } set widthCoupling(widthCoupling) { // Not using @damageArrayField so that accessor can be overridden if (this._widthCoupling !== widthCoupling) { this._widthCoupling = widthCoupling; this._layoutDirty = true; } } /** * Is the height coupled to the child's? If not {@link AxisCoupling.None}, * height constraints will be ignored or augmented. */ get heightCoupling() { return this._heightCoupling; } set heightCoupling(heightCoupling) { // Not using @damageArrayField so that accessor can be overridden if (this._heightCoupling !== heightCoupling) { this._heightCoupling = heightCoupling; this._layoutDirty = true; } } getBoundsOf(widget) { const [width, height] = widget.idealDimensions; const [x, y] = widget.idealPosition; const [childX, childY] = this.child.idealPosition; const left = this.idealX + this.offset[0] + x - childX; const top = this.idealY + this.offset[1] + y - childY; return [left, left + width, top, top + height]; } handleEvent(event) { if (event.propagation === PropagationModel.Trickling) { return this.internalViewport.dispatchTricklingEvent(event); } else { return super.handleEvent(event); } } handlePreLayoutUpdate() { // Pre-layout update child const child = this.child; child.preLayoutUpdate(); // If child's layout is dirty, set self's layout as dirty if (child.layoutDirty) { this._layoutDirty = true; } // Update viewport resolution if needed if (this.useCanvas) { this.internalViewport.resolution = this.root.resolution; } } handlePostLayoutUpdate() { // Post-layout update child const child = this.child; child.postLayoutUpdate(); } finalizeBounds() { super.finalizeBounds(); // Update viewport rect this.internalViewport.rect = [this.x, this.y, this.width, this.height]; } /** * Resolve the dimensions of the viewport widget, taking coupling modes and * reserved space into account. Note that if space is reserved, then the * resulting {@link ViewportWidget#idealWidth} and * {@link ViewportWidget#idealHeight} will not include the reserved space. * Child classes are expected to add the reserved space to the final * dimensions themselves so that they can also be aware of the final * non-reserved space. */ handleResolveDimensions(minWidth, maxWidth, minHeight, maxHeight) { // reserve space const rMaxWidth = Math.max(maxWidth - this.reservedX, 0); const rMaxHeight = Math.max(maxHeight - this.reservedY, 0); let effectiveMinWidth = Math.min(Math.max(minWidth - this.reservedX, 0), rMaxWidth); let effectiveMinHeight = Math.min(Math.max(minHeight - this.reservedY, 0), rMaxHeight); // Expand to the needed dimensions if (this._widthCoupling !== AxisCoupling.Bi && this._widthCoupling !== AxisCoupling.ChildToParent) { this.idealWidth = effectiveMinWidth; if (this._widthCoupling === AxisCoupling.ParentToChild) { effectiveMinWidth = this.idealWidth; } } if (this.idealWidth === 0 && this.minWidth === 0 && this._widthCoupling !== AxisCoupling.Bi) { console.warn(DynMsg.MAYBE_DIMENSIONLESS('width')); } if (this._heightCoupling !== AxisCoupling.Bi && this._heightCoupling !== AxisCoupling.ChildToParent) { this.idealHeight = effectiveMinHeight; if (this._heightCoupling === AxisCoupling.ParentToChild) { effectiveMinHeight = this.idealHeight; } } if (this.idealHeight === 0 && this.minHeight === 0 && this._heightCoupling !== AxisCoupling.Bi) { console.warn(DynMsg.MAYBE_DIMENSIONLESS('height')); } // Resolve child's layout and handle coupling const constraints = [...this._constraints]; if (this._widthCoupling !== AxisCoupling.None) { constraints[0] = effectiveMinWidth; if (this._widthCoupling === AxisCoupling.Bi) { constraints[1] = rMaxWidth; } } if (this._heightCoupling !== AxisCoupling.None) { constraints[2] = effectiveMinHeight; if (this._heightCoupling === AxisCoupling.Bi) { constraints[3] = rMaxHeight; } } const child = this.child; this.internalViewport.constraints = constraints; this.internalViewport.resolveLayout(); // Bi-couple wanted axes. Do regular layout for non-coupled axes. if (this._widthCoupling === AxisCoupling.Bi || this._widthCoupling === AxisCoupling.ChildToParent) { this.idealWidth = Math.max(0, child.idealDimensions[0]); } if (this._heightCoupling === AxisCoupling.Bi || this._heightCoupling === AxisCoupling.ChildToParent) { this.idealHeight = Math.max(0, child.idealDimensions[1]); } } attach(root, viewport, parent) { // HACK Parent#attach attaches child widgets with this._viewport, but // we want to use this.internalViewport Widget.prototype.attach.call(this, root, viewport, parent); this.internalViewport.parent = viewport; this.child.attach(root, this.internalViewport, this); } detach() { // unset parent viewport of internal viewport. using a clipped viewport // after this will crash; make sure to only use the viewport if the // widget is active this.internalViewport.parent = null; super.detach(); } handlePainting(dirtyRects) { // Clear background and paint canvas this.internalViewport.paint(dirtyRects); } propagateDirtyRect(rect) { const clippedRect = clipRelativeRectToAbsoluteViewport(this.internalViewport, this.rect, rect); if (!clippedRect) { return; } super.propagateDirtyRect(clippedRect); } queryRect(rect, relativeTo = null) { // convert rect from relative coordinates to absolute coordinates if // necessary if (this.internalViewport.relativeCoordinates) { rect = viewportRelativeRectToAbsolute(this.internalViewport, rect); } return super.queryRect(rect, relativeTo); } queryPoint(x, y, relativeTo = null) { // convert point from relative coordinates to absolute coordinates if // necessary if (this.internalViewport.relativeCoordinates) { [x, y] = viewportRelativePointToAbsolute(this.internalViewport, x, y); } return super.queryPoint(x, y, relativeTo); } } ViewportWidget.autoXML = { name: 'viewport-widget', inputConfig: SingleParentXMLInputConfig }; //# sourceMappingURL=ViewportWidget.js.map