stage-js
Version:
2D HTML5 Rendering and Layout
1,198 lines (1,019 loc) • 30 kB
text/typescript
import stats from "../common/stats";
import { Vec2Value } from "../common/matrix";
import { uid } from "../common/uid";
import { getDevicePixelRatio } from "../common/browser";
import { Pin, Pinned, FitMode } from "./pin";
import { Transition, TransitionOptions } from "./transition";
// todo: why there are two iids (other in pin)?
/** @internal */
let iid = 0;
stats.create = 0;
/** @internal */
function assertType<T>(obj: T): T {
if (obj && obj instanceof Component) {
return obj;
}
throw "Invalid component: " + obj;
}
interface ComponentVisitor<D> {
reverse?: boolean;
visible?: boolean;
start?: (component: Component, data?: D) => boolean | void;
end?: (component: Component, data?: D) => boolean | void;
}
export type ComponentTickListener<T> = (
this: T,
elapsed: number,
now: number,
last: number,
) => boolean | void;
export type ComponentEventListener<T> = (this: T, ...args: any[]) => void;
/** @hidden @deprecated Use component() */
export function create() {
return component();
}
/** @hidden @deprecated Use maximize() */
export function layer() {
return maximize();
}
/** @hidden @deprecated Use minimize() */
export function box() {
return minimize();
}
// todo: create a new subclass called layout, and make component abstract
// discussion: in some cases sprites are used as parent component, like a window
/** @hidden @deprecated */
export function layout() {
return component();
}
export function component() {
return new Component();
}
export function row(align: number) {
return new Component().row(align).label("Row");
}
export function column(align: number) {
return new Component().column(align).label("Column");
}
export function minimize() {
return new Component().minimize().label("Minimize");
}
export function maximize() {
return new Component().maximize().label("Maximize");
}
// TODO: do not clear next/prev/parent on remove (why?)
// There are three sets of core functions:
// - tree model manipulation functions
// - frame loop and rendering
// - events handling
export class Component implements Pinned {
/** @internal */ uid = "component:" + uid();
/** @internal */ _label = "";
/** @internal */ _parent: Component | null = null;
/** @internal */ _next: Component | null = null;
/** @internal */ _prev: Component | null = null;
/** @internal */ _first: Component | null = null;
/** @internal */ _last: Component | null = null;
/** @internal */ _visible = true;
// this is computed on every render, and used by children
/** @internal */ _alpha: number = 1;
/** @internal */ _padding: number = 0;
/** @internal */ _spacing: number = 0;
/** @internal */ _pin = new Pin(this);
/** @internal */ _ts_pin: number;
/** @internal */ _ts_parent: number;
/** @internal */ _ts_children: number;
/** @internal */ _ts_touch: number;
// todo: don't need to check if these fields are initialized anymore
/** @internal */ _listeners: Record<string, ComponentEventListener<Component>[]> = {};
/** @internal */ _attrs: Record<string, any> = {};
/** @internal */ _flags: Record<string, any> = {};
/** @internal */ _transitions: Transition[] = [];
/** @internal */ _tickBefore: ComponentTickListener<any>[] = [];
/** @internal */ _tickAfter: ComponentTickListener<any>[] = [];
/** @internal */ _layoutTicker?: () => void;
// todo: remove this
MAX_ELAPSE = Infinity;
/** @internal */ _mo_seq: number;
/** @internal */ _mo_seqAlign: number;
/** @internal */ _mo_box: number;
constructor() {
stats.create++;
if (this instanceof Component) {
this.label(this.constructor.name);
}
}
matrix(relative = false) {
if (relative === true) {
return this._pin.relativeMatrix();
}
return this._pin.absoluteMatrix();
}
/** @hidden @deprecated */
getPixelRatio() {
// todo: remove this function
const m = this._parent?.matrix();
const pixelRatio = !m ? 1 : Math.max(Math.abs(m.a), Math.abs(m.b)) / getDevicePixelRatio();
return pixelRatio;
}
/** @hidden This is not accurate before first tick */
getDevicePixelRatio() {
// todo: parent matrix is not available in the first call
const parentMatrix = this._parent?.matrix();
const pixelRatio = !parentMatrix
? 1
: Math.max(Math.abs(parentMatrix.a), Math.abs(parentMatrix.b));
return pixelRatio;
}
/** @hidden This is not accurate before first tick */
getLogicalPixelRatio() {
return this.getDevicePixelRatio() / getDevicePixelRatio();
}
pin(key: string): any;
pin(key: string, value: any): this;
pin(obj: object): this;
pin(): Pin;
pin(a?: object | string, b?: any) {
if (typeof a === "object") {
this._pin.set(a);
return this;
} else if (typeof a === "string") {
if (typeof b === "undefined") {
return this._pin.get(a);
} else {
this._pin.set(a, b);
return this;
}
} else if (typeof a === "undefined") {
return this._pin;
}
}
fit(width: number, height: number, mode?: FitMode): this;
fit(fit: object): this;
fit(a, b?, c?) {
if (typeof a === "object") {
c = b;
b = a.y;
a = a.x;
}
this._pin.fit(a, b, c);
return this;
}
/** @hidden @deprecated Use fit */
scaleTo(a, b?, c?): this {
return this.fit(a, b, c);
}
toString() {
return "[" + this._label + "]";
}
/** @hidden @deprecated Use label() */
id(): string;
/** @hidden @deprecated Use label() */
id(label: string): this;
/** @hidden @deprecated Use label() */
id(label?: string) {
if (typeof label === "undefined") {
return this._label;
}
this._label = label;
return this;
}
label(): string;
label(label: string): this;
label(label?: string) {
if (typeof label === "undefined") {
return this._label;
}
this._label = label;
return this;
}
attr(name: string, value: any): this;
attr(name: string): any;
attr(name: string, value?: any) {
if (typeof value === "undefined") {
return this._attrs !== null ? this._attrs[name] : undefined;
}
(this._attrs !== null ? this._attrs : (this._attrs = {}))[name] = value;
return this;
}
visible(visible: boolean): this;
visible(): boolean;
visible(visible?: boolean) {
if (typeof visible === "undefined") {
return this._visible;
}
this._visible = visible;
this._parent && (this._parent._ts_children = ++iid);
this._ts_pin = ++iid;
this.touch();
return this;
}
hide() {
this.visible(false);
return this;
}
show() {
this.visible(true);
return this;
}
parent() {
return this._parent;
}
next(visible?: boolean) {
let next = this._next;
while (next && visible && !next._visible) {
next = next._next;
}
return next;
}
prev(visible?: boolean) {
let prev = this._prev;
while (prev && visible && !prev._visible) {
prev = prev._prev;
}
return prev;
}
first(visible?: boolean) {
let next = this._first;
while (next && visible && !next._visible) {
next = next._next;
}
return next;
}
last(visible?: boolean) {
let prev = this._last;
while (prev && visible && !prev._visible) {
prev = prev._prev;
}
return prev;
}
visit<P>(visitor: ComponentVisitor<P>, payload?: P) {
const reverse = visitor.reverse;
const visible = visitor.visible;
if (visitor.start && visitor.start(this, payload)) {
return;
}
let child: Component;
let next = reverse ? this.last(visible) : this.first(visible);
while ((child = next)) {
next = reverse ? child.prev(visible) : child.next(visible);
if (child.visit(visitor, payload)) {
return true;
}
}
return visitor.end && visitor.end(this, payload);
}
append(...child: Component[]): this;
append(child: Component[]): this;
append(child: Component | Component[], more?: Component) {
if (Array.isArray(child)) {
for (let i = 0; i < child.length; i++) {
Component.append(this, child[i]);
}
} else if (typeof more !== "undefined") {
// deprecated
for (let i = 0; i < arguments.length; i++) {
Component.append(this, arguments[i]);
}
} else if (typeof child !== "undefined") Component.append(this, child);
return this;
}
prepend(...child: Component[]): this;
prepend(child: Component[]): this;
prepend(child: Component | Component[], more?: Component) {
if (Array.isArray(child)) {
for (let i = child.length - 1; i >= 0; i--) {
Component.prepend(this, child[i]);
}
} else if (typeof more !== "undefined") {
// deprecated
for (let i = arguments.length - 1; i >= 0; i--) {
Component.prepend(this, arguments[i]);
}
} else if (typeof child !== "undefined") Component.prepend(this, child);
return this;
}
appendTo(parent: Component) {
Component.append(parent, this);
return this;
}
prependTo(parent: Component) {
Component.prepend(parent, this);
return this;
}
insertNext(sibling: Component, more?: Component) {
if (Array.isArray(sibling)) {
for (let i = 0; i < sibling.length; i++) {
Component.insertAfter(sibling[i], this);
}
} else if (typeof more !== "undefined") {
// deprecated
for (let i = 0; i < arguments.length; i++) {
Component.insertAfter(arguments[i], this);
}
} else if (typeof sibling !== "undefined") {
Component.insertAfter(sibling, this);
}
return this;
}
insertPrev(sibling: Component, more?: Component) {
if (Array.isArray(sibling)) {
for (let i = sibling.length - 1; i >= 0; i--) {
Component.insertBefore(sibling[i], this);
}
} else if (typeof more !== "undefined") {
// deprecated
for (let i = arguments.length - 1; i >= 0; i--) {
Component.insertBefore(arguments[i], this);
}
} else if (typeof sibling !== "undefined") {
Component.insertBefore(sibling, this);
}
return this;
}
insertAfter(prev: Component) {
Component.insertAfter(this, prev);
return this;
}
insertBefore(next: Component) {
Component.insertBefore(this, next);
return this;
}
/** @internal */
static append(parent: Component, child: Component) {
assertType(child);
assertType(parent);
child.remove();
if (parent._last) {
parent._last._next = child;
child._prev = parent._last;
}
child._parent = parent;
parent._last = child;
if (!parent._first) {
parent._first = child;
}
child._parent._flag(child, true);
child._ts_parent = ++iid;
parent._ts_children = ++iid;
parent.touch();
}
/** @internal */
static prepend(parent: Component, child: Component) {
assertType(child);
assertType(parent);
child.remove();
if (parent._first) {
parent._first._prev = child;
child._next = parent._first;
}
child._parent = parent;
parent._first = child;
if (!parent._last) {
parent._last = child;
}
child._parent._flag(child, true);
child._ts_parent = ++iid;
parent._ts_children = ++iid;
parent.touch();
}
/** @internal */
static insertBefore(self: Component, next: Component) {
assertType(self);
assertType(next);
self.remove();
const parent = next._parent;
const prev = next._prev;
if (!parent) {
return;
}
next._prev = self;
// todo:
(prev && (prev._next = self)) || (parent && (parent._first = self));
self._parent = parent;
self._prev = prev;
self._next = next;
self._parent._flag(self, true);
self._ts_parent = ++iid;
self.touch();
}
/** @internal */
static insertAfter(self: Component, prev: Component) {
assertType(self);
assertType(prev);
self.remove();
const parent = prev._parent;
const next = prev._next;
if (!parent) {
return;
}
prev._next = self;
// todo:
(next && (next._prev = self)) || (parent && (parent._last = self));
self._parent = parent;
self._prev = prev;
self._next = next;
self._parent._flag(self, true);
self._ts_parent = ++iid;
self.touch();
}
remove(child?: Component, more?: any) {
if (typeof child !== "undefined") {
if (Array.isArray(child)) {
for (let i = 0; i < child.length; i++) {
assertType(child[i]).remove();
}
} else if (typeof more !== "undefined") {
for (let i = 0; i < arguments.length; i++) {
assertType(arguments[i]).remove();
}
} else {
assertType(child).remove();
}
return this;
}
if (this._prev) {
this._prev._next = this._next;
}
if (this._next) {
this._next._prev = this._prev;
}
if (this._parent) {
if (this._parent._first === this) {
this._parent._first = this._next;
}
if (this._parent._last === this) {
this._parent._last = this._prev;
}
this._parent._flag(this, false);
this._parent._ts_children = ++iid;
this._parent.touch();
}
this._prev = this._next = this._parent = null;
this._ts_parent = ++iid;
// this._parent.touch();
return this;
}
empty() {
let child: Component | null = null;
let next = this._first;
while ((child = next)) {
next = child._next;
child._prev = child._next = child._parent = null;
this._flag(child, false);
}
this._first = this._last = null;
this._ts_children = ++iid;
this.touch();
return this;
}
touch() {
this._ts_touch = ++iid;
this._parent && this._parent.touch();
return this;
}
/** @internal */
_flag(child: Component, value: boolean): Component;
/** @internal */
_flag(key: string): boolean;
/** @internal */
_flag(key: string, value: boolean): Component;
/** @internal Deep flag, used for optimizing event distribution. */
_flag(key: string | Component, value?: boolean) {
if (typeof value === "undefined") {
return (this._flags !== null && this._flags[key as string]) || 0;
}
if (typeof key === "string") {
if (value) {
this._flags = this._flags || {};
if (!this._flags[key] && this._parent) {
this._parent._flag(key, true);
}
this._flags[key] = (this._flags[key] || 0) + 1;
} else if (this._flags && this._flags[key] > 0) {
if (this._flags[key] == 1 && this._parent) {
this._parent._flag(key, false);
}
this._flags[key] = this._flags[key] - 1;
}
}
if (typeof key === "object") {
if (key._flags) {
for (const type in key._flags) {
if (key._flags[type] > 0) {
this._flag(type, value);
}
}
}
}
return this;
}
/** @internal */
hitTest(hit: Vec2Value) {
const width = this._pin._width;
const height = this._pin._height;
return hit.x >= 0 && hit.x <= width && hit.y >= 0 && hit.y <= height;
}
/** @hidden */
prerender() {
if (!this._visible) {
return;
}
this.prerenderTexture();
let child: Component;
let next = this._first;
while ((child = next)) {
next = child._next;
child.prerender();
}
}
/** @hidden */
prerenderTexture() {
// to be implemented by subclasses if needed
}
/** @hidden */
private renderedBefore = false;
/** @hidden */
render(context: CanvasRenderingContext2D) {
if (!this._visible) {
return;
}
stats.component++;
const m = this.matrix();
context.setTransform(m.a, m.b, m.c, m.d, m.e, m.f);
// move this elsewhere!
this._alpha = this._pin._alpha * (this._parent ? this._parent._alpha : 1);
const alpha = this._pin._textureAlpha * this._alpha;
if (context.globalAlpha != alpha) {
context.globalAlpha = alpha;
}
if (!this.renderedBefore) {
// todo: because getDevicePixelRatio is not accurate before first tick
this.prerenderTexture();
}
this.renderedBefore = true;
this.renderTexture(context);
if (context.globalAlpha != this._alpha) {
context.globalAlpha = this._alpha;
}
let child: Component;
let next = this._first;
while ((child = next)) {
next = child._next;
child.render(context);
}
}
/** @hidden */
renderTexture(context: CanvasRenderingContext2D) {
// to be implemented by subclasses if needed
}
/** @internal */
_tick(elapsed: number, now: number, last: number) {
if (!this._visible) {
return;
}
if (elapsed > this.MAX_ELAPSE) {
elapsed = this.MAX_ELAPSE;
}
let ticked = false;
if (this._tickBefore !== null) {
for (let i = 0; i < this._tickBefore.length; i++) {
stats.tick++;
const tickFn = this._tickBefore[i];
ticked = tickFn.call(this, elapsed, now, last) === true || ticked;
}
}
let child: Component | null;
let next = this._first;
while ((child = next)) {
next = child._next;
if (child._flag("_tick")) {
ticked = child._tick(elapsed, now, last) === true ? true : ticked;
}
}
if (this._tickAfter !== null) {
for (let i = 0; i < this._tickAfter.length; i++) {
stats.tick++;
const tickFn = this._tickAfter[i];
ticked = tickFn.call(this, elapsed, now, last) === true || ticked;
}
}
return ticked;
}
tick(callback: ComponentTickListener<this>, before = false) {
if (typeof callback !== "function") {
return;
}
if (before) {
if (this._tickBefore === null) {
this._tickBefore = [];
}
this._tickBefore.push(callback);
} else {
if (this._tickAfter === null) {
this._tickAfter = [];
}
this._tickAfter.push(callback);
}
const hasTickListener = this._tickAfter?.length > 0 || this._tickBefore?.length > 0;
this._flag("_tick", hasTickListener);
}
untick(callback: ComponentTickListener<this>) {
if (typeof callback !== "function") {
return;
}
let i;
if (this._tickBefore !== null && (i = this._tickBefore.indexOf(callback)) >= 0) {
this._tickBefore.splice(i, 1);
}
if (this._tickAfter !== null && (i = this._tickAfter.indexOf(callback)) >= 0) {
this._tickAfter.splice(i, 1);
}
}
timeout(callback: () => any, time: number) {
this.setTimeout(callback, time);
}
setTimeout(callback: () => any, time: number) {
function timer(t: number) {
if ((time -= t) < 0) {
this.untick(timer);
callback.call(this);
} else {
return true;
}
}
this.tick(timer);
return timer;
}
clearTimeout(timer: ComponentTickListener<this>) {
this.untick(timer);
}
on(types: string, listener: ComponentEventListener<this>): this;
/** @hidden @deprecated @internal */
on(types: string[], listener: ComponentEventListener<this>): this;
on(type: string | string[], listener: ComponentEventListener<this>) {
if (!type || !type.length || typeof listener !== "function") {
return this;
}
if (typeof type !== "string" && typeof type.join === "function") {
// deprecated arguments, type is array
for (let i = 0; i < type.length; i++) {
this.on(type[i], listener);
}
} else if (typeof type === "string" && type.indexOf(" ") > -1) {
// deprecated arguments, type is spaced string
type = type.match(/\S+/g);
for (let i = 0; i < type.length; i++) {
this._on(type[i], listener);
}
} else if (typeof type === "string") {
this._on(type, listener);
} else {
// invalid
}
return this;
}
/** @internal */
_on(type: string, listener: ComponentEventListener<this>) {
if (typeof type !== "string" && typeof listener !== "function") {
return;
}
this._listeners[type] = this._listeners[type] || [];
this._listeners[type].push(listener);
// todo: maybe recompute/set exact value?
this._flag(type, true);
}
off(types: string, listener: ComponentEventListener<this>): this;
/** @hidden @deprecated @internal */
off(types: string[], listener: ComponentEventListener<this>): this;
off(type: string | string[], listener: ComponentEventListener<this>) {
if (!type || !type.length || typeof listener !== "function") {
return this;
}
if (typeof type !== "string" && typeof type.join === "function") {
// deprecated arguments, type is array
for (let i = 0; i < type.length; i++) {
this.off(type[i], listener);
}
} else if (typeof type === "string" && type.indexOf(" ") > -1) {
// deprecated arguments, type is spaced string
type = type.match(/\S+/g);
for (let i = 0; i < type.length; i++) {
this._off(type[i], listener);
}
} else if (typeof type === "string") {
this._off(type, listener);
} else {
// invalid
}
return this;
}
/** @internal */
_off(type: string, listener: ComponentEventListener<this>) {
if (typeof type !== "string" && typeof listener !== "function") {
return;
}
const listeners = this._listeners[type];
if (!listeners || !listeners.length) {
return;
}
const index = listeners.indexOf(listener);
if (index >= 0) {
listeners.splice(index, 1);
// if (!listeners.length) {
// delete this._listeners[type];
// }
// todo: maybe recompute/set exact value?
this._flag(type, false);
}
}
listeners(type: string) {
return this._listeners[type];
}
publish(name: string, args?: any) {
const listeners = this.listeners(name);
if (!listeners || !listeners.length) {
return 0;
}
for (let l = 0; l < listeners.length; l++) {
listeners[l].apply(this, args);
}
return listeners.length;
}
/** @hidden @deprecated @internal */
trigger(name: string, args?: any) {
this.publish(name, args);
return this;
}
size(w: number, h: number) {
// Pin shortcut, used by Transition and Component
this.pin("width", w);
this.pin("height", h);
return this;
}
width(w: number): this;
width(): number;
width(w?: number) {
// Pin shortcut, used by Transition and Component
if (typeof w === "undefined") {
return this.pin("width");
}
this.pin("width", w);
return this;
}
height(h: number): this;
height(): number;
height(h?: number) {
// Pin shortcut, used by Transition and Component
if (typeof h === "undefined") {
return this.pin("height");
}
this.pin("height", h);
return this;
}
offset(value: Vec2Value): this;
offset(x: number, y: number): this;
offset(a?: Vec2Value | number, b?: number) {
// Pin shortcut, used by Transition and Component
if (typeof a === "object") {
b = a.y;
a = a.x;
}
this.pin("offsetX", a);
this.pin("offsetY", b);
return this;
}
rotate(a: number) {
// Pin shortcut, used by Transition and Component
this.pin("rotation", a);
return this;
}
skew(value: Vec2Value): this;
skew(x: number, y: number): this;
skew(a?: Vec2Value | number, b?: number) {
// Pin shortcut, used by Transition and Component
if (typeof a === "object") {
b = a.y;
a = a.x;
} else if (typeof b === "undefined") b = a;
this.pin("skewX", a);
this.pin("skewY", b);
return this;
}
scale(value: Vec2Value): this;
scale(x: number, y: number): this;
scale(s: number): this;
scale(a?: Vec2Value | number, b?: number) {
// Pin shortcut, used by Transition and Component
if (typeof a === "object") {
b = a.y;
a = a.x;
} else if (typeof b === "undefined") b = a;
this.pin("scaleX", a);
this.pin("scaleY", b);
return this;
}
alpha(a: number, ta?: number) {
// Pin shortcut, used by Transition and Component
this.pin("alpha", a);
if (typeof ta !== "undefined") {
this.pin("textureAlpha", ta);
}
return this;
}
tween(opts?: TransitionOptions): Transition;
tween(duration?: number, delay?: number, append?: boolean): Transition;
tween(a?: object | number, b?: number, c?: boolean) {
let options: TransitionOptions;
if (typeof a === "object" && a !== null) {
options = a;
} else {
options = {};
if (typeof a === "number") {
options.duration = a;
if (typeof b === "number") {
options.delay = b;
if (typeof c === "boolean") {
options.append = c;
}
} else if (typeof b === "boolean") {
options.append = b;
}
} else if (typeof a === "boolean") {
options.append = a;
}
}
if (!this._transitionTickInitied) {
this.tick(this._transitionTick, true);
this._transitionTickInitied = true;
}
this.touch();
// todo: what is the expected default behavior?
if (!options.append) {
this._transitions.length = 0;
}
const transition = new Transition(this, options);
this._transitions.push(transition);
return transition;
}
/** @internal */ _transitionTickInitied = false;
/** @internal */ _transitionTickLastTime = 0;
/** @internal */
_transitionTick = (elapsed: number, now: number, last: number) => {
if (!this._transitions.length) {
return false;
}
// ignore untracked tick
const ignore = this._transitionTickLastTime !== last;
this._transitionTickLastTime = now;
if (ignore) {
return true;
}
const head = this._transitions[0];
const ended = head.tick(this, elapsed, now, last);
// todo: move this logic to TransitionQueue
if (ended) {
if (head === this._transitions[0]) {
this._transitions.shift();
}
const next = head.finish();
if (next) {
this._transitions.unshift(next);
}
}
return true;
};
row(align: number) {
this.align("row", align);
return this;
}
column(align: number) {
this.align("column", align);
return this;
}
align(type: "row" | "column", align: number) {
this._padding = this._padding;
this._spacing = this._spacing;
this._layoutTicker && this.untick(this._layoutTicker);
this.tick(
(this._layoutTicker = () => {
if (this._mo_seq == this._ts_touch) {
return;
}
this._mo_seq = this._ts_touch;
const alignChildren = this._mo_seqAlign != this._ts_children;
this._mo_seqAlign = this._ts_children;
let width = 0;
let height = 0;
let child: Component;
let next = this.first(true);
let first = true;
while ((child = next)) {
next = child.next(true);
child.matrix(true);
const w = child.pin("boxWidth");
const h = child.pin("boxHeight");
if (type == "column") {
!first && (height += this._spacing);
child.pin("offsetY") != height && child.pin("offsetY", height);
width = Math.max(width, w);
height = height + h;
alignChildren && child.pin("alignX", align);
} else if (type == "row") {
!first && (width += this._spacing);
child.pin("offsetX") != width && child.pin("offsetX", width);
width = width + w;
height = Math.max(height, h);
alignChildren && child.pin("alignY", align);
}
first = false;
}
width += 2 * this._padding;
height += 2 * this._padding;
this.pin("width") != width && this.pin("width", width);
this.pin("height") != height && this.pin("height", height);
}),
);
return this;
}
/** @hidden @deprecated Use minimize() */
box() {
return this.minimize();
}
/** @hidden @deprecated Use minimize() */
layer() {
return this.maximize();
}
/**
* Set size to match largest child size.
*/
minimize() {
this._padding = this._padding;
this._layoutTicker && this.untick(this._layoutTicker);
this.tick(
(this._layoutTicker = () => {
if (this._mo_box == this._ts_touch) {
return;
}
this._mo_box = this._ts_touch;
let width = 0;
let height = 0;
let child: Component;
let next = this.first(true);
while ((child = next)) {
next = child.next(true);
child.matrix(true);
const w = child.pin("boxWidth");
const h = child.pin("boxHeight");
width = Math.max(width, w);
height = Math.max(height, h);
}
width += 2 * this._padding;
height += 2 * this._padding;
this.pin("width") != width && this.pin("width", width);
this.pin("height") != height && this.pin("height", height);
}),
);
return this;
}
/**
* Set size to match parent size.
*/
maximize() {
this._layoutTicker && this.untick(this._layoutTicker);
this.tick(
(this._layoutTicker = () => {
const parent = this.parent();
if (parent) {
const width = parent.pin("width");
if (this.pin("width") != width) {
this.pin("width", width);
}
const height = parent.pin("height");
if (this.pin("height") != height) {
this.pin("height", height);
}
}
}),
true,
);
return this;
}
// TODO: move padding to pin
/**
* Set cell spacing for layout.
*/
padding(pad: number) {
this._padding = pad;
return this;
}
/**
* Set cell spacing for row and column layout.
*/
spacing(space: number) {
this._spacing = space;
return this;
}
}
/** @hidden @deprecated Node is renamed to Component */
export { Component as Node };