/* * If not stated otherwise in this file or this component's LICENSE file the * following copyright and licenses apply: * * Copyright 2023 Comcast Cable Communications Management, LLC. * * Licensed under the Apache License, Version 2.0 (the License); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { UpdateType, type CoreNode } from './CoreNode.js'; import type { Coord } from './lib/utils.js'; export enum AutosizeMode { Children = 0, Texture = 1, } export enum AutosizeUpdateType { None = 0, Filtered = 1, All = 2, } const applyDimensions = (node: CoreNode, w: number, h: number) => { node.props.w = w; node.props.h = h; node.setUpdateType(UpdateType.Local); }; const getFilteredChildren = ( children: number[], childMap: Map, ) => { const filtered: CoreNode[] = []; while (children.length > 0) { const id = children.pop()!; const child = childMap.get(id)!; filtered.push(child); } return filtered; }; let autosizerId = 0; export class Autosizer { public id = autosizerId++; mode: AutosizeMode = AutosizeMode.Children; updateType: AutosizeUpdateType = AutosizeUpdateType.All; lastWidth: number = 0; lastHeight: number = 0; lastHasChanged: boolean = false; flaggedChildren: number[] = []; childMap: Map = new Map(); minX = Infinity; minY = Infinity; maxX = -Infinity; maxY = -Infinity; corners: [Coord, Coord, Coord, Coord] = [ { x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }, ]; constructor(public node: CoreNode) { if (node.texture !== null) { this.mode = AutosizeMode.Texture; } } attach(node: CoreNode) { this.childMap.set(node.id, node); node.parentAutosizer = this; //bubble down to attach to grandchildren if (node.children.length > 0 && node.autosizer === null) { const children = node.children; for (let i = 0; i < children.length; i++) { this.attach(children[i]!); } } } detach(node: CoreNode) { if (this.childMap.delete(node.id) === true) { node.parentAutosizer = null; if (node.children.length > 0 && node.autosizer === null) { const children = node.children; for (let i = 0; i < children.length; i++) { this.detach(children[i]!); } } //detached a child, need full update this.setUpdateType(AutosizeUpdateType.All); } } patch(id: number) { const entry = this.childMap.get(id); if (entry === undefined) { return; } this.flaggedChildren.push(id); this.setUpdateType(AutosizeUpdateType.Filtered); } setUpdateType(updateType: AutosizeUpdateType) { this.updateType |= updateType; this.node.setUpdateType(UpdateType.Autosize); } setMode(mode: AutosizeMode) { this.mode = mode; this.setUpdateType(AutosizeUpdateType.All); } update() { const node = this.node; if ( this.mode === AutosizeMode.Texture && node.texture !== null && node.texture.dimensions !== null ) { const { w, h } = node.texture.dimensions; if (w !== node.w || h !== node.h) { applyDimensions(node, w, h); } this.lastWidth = w; this.lastHeight = h; this.updateType = AutosizeUpdateType.None; return; } let filtered: CoreNode[] = this.updateType === AutosizeUpdateType.Filtered ? getFilteredChildren(this.flaggedChildren, this.childMap) : Array.from(this.childMap.values()); if (filtered.length === 0) { return; } const corners = this.corners; let minX = this.minX; let minY = this.minY; let maxX = this.maxX; let maxY = this.maxY; for (let i = 0; i < filtered.length; i++) { const child = filtered[i]!; if (child.isRenderable === false || child.localTransform === undefined) { continue; } const { tx, ty, ta, tb, tc, td } = child.localTransform; const w = child.props.w; const h = child.props.h; const childMinX = tx; const childMaxX = tx + w * ta; const childMinY = ty; const childMaxY = ty + h * td; corners[0].x = childMinX; corners[0].y = childMinY; corners[1].x = childMaxX; //no rotation/scale if (tb === 0 && tc === 0) { corners[1].y = childMinY; corners[2].x = childMaxX; corners[2].y = childMaxY; corners[3].x = childMinX; corners[3].y = childMaxY; } else { corners[1].y = tx + w * tc; corners[2].x = tx + w * ta + h * tb; corners[2].y = ty + w * tc + h * td; corners[3].x = tx + h * tb; corners[3].y = ty + h * td; } for (let j = 0; j < 4; j++) { const corner = corners[j]!; if (corner.x < minX) minX = corner.x; if (corner.y < minY) minY = corner.y; if (corner.x > maxX) maxX = corner.x; if (corner.y > maxY) maxY = corner.y; } } this.updateType = AutosizeUpdateType.None; const newWidth = maxX > 0 ? maxX : 0; const newHeight = maxY > 0 ? maxY : 0; applyDimensions(node, newWidth, newHeight); this.lastWidth = newWidth; this.lastHeight = newHeight; } destroy() { if (this.childMap.size > 0) { for (const child of this.childMap.values()) { child.parentAutosizer = null; } } this.childMap.clear(); this.flaggedChildren.length = 0; } }