UNPKG

sprotty

Version:

A next-gen framework for graphical views

168 lines (154 loc) 7.78 kB
/******************************************************************************** * Copyright (c) 2017-2024 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * This Source Code may also be made available under the following Secondary * Licenses when the conditions for such availability set forth in the Eclipse * Public License v. 2.0 are satisfied: GNU General Public License, version 2 * with the GNU Classpath Exception which is available at * https://www.gnu.org/software/classpath/license.html. * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { Bounds, Dimension, Point } from 'sprotty-protocol/lib/utils/geometry'; import { SParentElementImpl, SModelElementImpl, SChildElementImpl } from '../../base/model/smodel'; import { isLayoutContainer, isLayoutableChild, InternalLayoutContainer, isBoundsAware } from './model'; import { ILayout, StatefulLayouter } from './layout'; import { AbstractLayoutOptions } from './layout-options'; import { BoundsData } from './hidden-bounds-updater'; import { injectable } from 'inversify'; import { HAlignment, VAlignment } from 'sprotty-protocol/lib/model'; @injectable() export abstract class AbstractLayout<T extends AbstractLayoutOptions> implements ILayout { layout(container: SParentElementImpl & InternalLayoutContainer, layouter: StatefulLayouter) { const boundsData = layouter.getBoundsData(container); const options = this.getLayoutOptions(container); const childrenSize = this.getChildrenSize(container, options, layouter); const maxWidth = options.paddingFactor * ( options.resizeContainer ? Math.max(childrenSize.width, options.minWidth) : Math.max(0, this.getFixedContainerBounds(container, options, layouter).width) - options.paddingLeft - options.paddingRight); const maxHeight = options.paddingFactor * ( options.resizeContainer ? Math.max(childrenSize.height, options.minHeight) : Math.max(0, this.getFixedContainerBounds(container, options, layouter).height) - options.paddingTop - options.paddingBottom); if (maxWidth > 0 && maxHeight > 0) { const offset = this.layoutChildren(container, layouter, options, maxWidth, maxHeight); boundsData.bounds = this.getFinalContainerBounds(container, offset, options, maxWidth, maxHeight); boundsData.boundsChanged = true; } } protected abstract layoutChild(child: SChildElementImpl, boundsData: BoundsData, bounds: Bounds, childOptions: T, containerOptions: T, currentOffset: Point, maxWidth: number, maxHeight: number): Point; protected getFinalContainerBounds(container: SParentElementImpl & InternalLayoutContainer, lastOffset: Point, options: T, maxWidth: number, maxHeight: number): Bounds { return { x: container.bounds.x, y: container.bounds.y, width: Math.max(options.minWidth, maxWidth + options.paddingLeft + options.paddingRight), height: Math.max(options.minHeight, maxHeight + options.paddingTop + options.paddingBottom) }; } protected getFixedContainerBounds( container: SModelElementImpl, layoutOptions: T, layouter: StatefulLayouter): Bounds { let currentContainer = container; while (true) { if (isBoundsAware(currentContainer)) { const bounds = currentContainer.bounds; if (isLayoutContainer(currentContainer) && layoutOptions.resizeContainer) layouter.log.error(currentContainer, 'Resizable container found while detecting fixed bounds'); if (Dimension.isValid(bounds)) return bounds; } if (currentContainer instanceof SChildElementImpl) { currentContainer = currentContainer.parent; } else { layouter.log.error(currentContainer, 'Cannot detect fixed bounds'); return Bounds.EMPTY; } } } protected abstract getChildrenSize(container: SParentElementImpl & InternalLayoutContainer, containerOptions: T, layouter: StatefulLayouter): Dimension; protected layoutChildren(container: SParentElementImpl & InternalLayoutContainer, layouter: StatefulLayouter, containerOptions: T, maxWidth: number, maxHeight: number): Point { let currentOffset: Point = { x: containerOptions.paddingLeft + 0.5 * (maxWidth - (maxWidth / containerOptions.paddingFactor)), y: containerOptions.paddingTop + 0.5 * (maxHeight - (maxHeight / containerOptions.paddingFactor))}; container.children.forEach( child => { if (isLayoutableChild(child)) { const boundsData = layouter.getBoundsData(child); const bounds = boundsData.bounds; const childOptions = this.getChildLayoutOptions(child, containerOptions); if (bounds !== undefined && Dimension.isValid(bounds)) { currentOffset = this.layoutChild(child, boundsData, bounds, childOptions, containerOptions, currentOffset, maxWidth, maxHeight); } } } ); return currentOffset; } protected getDx(hAlign: HAlignment, bounds: Bounds, maxWidth: number): number { switch (hAlign) { case 'left': return 0; case 'center': return 0.5 * (maxWidth - bounds.width); case 'right': return maxWidth - bounds.width; } } protected getDy(vAlign: VAlignment, bounds: Bounds, maxHeight: number): number { switch (vAlign) { case 'top': return 0; case 'center': return 0.5 * (maxHeight - bounds.height); case 'bottom': return maxHeight - bounds.height; } } protected getChildLayoutOptions(child: SChildElementImpl, containerOptions: T): T { const layoutOptions = (child as any).layoutOptions; if (layoutOptions === undefined) return containerOptions; else return this.spread(containerOptions, layoutOptions); } protected getLayoutOptions(element: SModelElementImpl): T { let current = element; const allOptions: T[] = []; while (current !== undefined) { const layoutOptions = (current as any).layoutOptions; if (layoutOptions !== undefined) allOptions.push(layoutOptions); if (current instanceof SChildElementImpl) current = current.parent; else break; } return allOptions.reverse().reduce( (a, b) => { return this.spread(a, b); }, this.getDefaultLayoutOptions()); } protected abstract getDefaultLayoutOptions(): T; protected abstract spread(a: T, b: T): T; }