lazy-widgets
Version:
Typescript retained mode GUI for the HTML canvas API
292 lines • 13.2 kB
JavaScript
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