@eclipse-scout/core
Version:
Eclipse Scout runtime
592 lines (566 loc) • 19.7 kB
text/typescript
/*
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {arrays, Dimension, HtmlComponent, HtmlCompPrefSizeOptions, InitModelOf, Insets, LayoutConstants, LogicalGridData, LogicalGridLayoutInfoModel, Rectangle, TreeSet} from '../../index';
import $ from 'jquery';
/**
* JavaScript port of org.eclipse.scout.rt.ui.swing.LogicalGridLayoutInfo.
*/
export class LogicalGridLayoutInfo implements LogicalGridLayoutInfoModel {
declare model: LogicalGridLayoutInfoModel;
gridDatas: LogicalGridData[];
cons: LogicalGridData[];
cols: number;
compSize: Dimension[];
rows: number;
width: number[][];
widthHints: number[];
height: number[][];
weightX: number[];
weightY: number[];
hgap: number;
vgap: number;
rowHeight: number;
columnWidth: number;
cellBounds: Rectangle[][];
widthHint: number;
widthOnly: boolean;
$components: JQuery[];
constructor(model: InitModelOf<LogicalGridLayoutInfo>) {
this.gridDatas = [];
this.$components = null;
this.cols = 0;
this.compSize = [];
this.rows = 0;
this.width = [];
this.widthHints = [];
this.height = [];
this.weightX = [];
this.weightY = [];
this.hgap = 0;
this.vgap = 0;
this.rowHeight = 0;
this.columnWidth = 0;
this.cellBounds = [];
this.widthHint = null;
this.widthOnly = false;
$.extend(this, model);
// create a modifiable copy of the grid datas
let i, gd, x, y;
for (i = 0; i < this.cons.length; i++) {
this.gridDatas[i] = new LogicalGridData(this.cons[i]);
}
if (this.$components.length === 0) {
return;
}
// eliminate unused rows and columns
let usedCols = new TreeSet();
let usedRows = new TreeSet();
// ticket 86645 use member gridDatas instead of param cons
for (i = 0; i < this.gridDatas.length; i++) {
gd = this.gridDatas[i];
if (gd.gridx < 0) {
gd.gridx = 0;
}
if (gd.gridy < 0) {
gd.gridy = 0;
}
if (gd.gridw < 1) {
gd.gridw = 1;
}
if (gd.gridh < 1) {
gd.gridh = 1;
}
for (x = gd.gridx; x < gd.gridx + gd.gridw; x++) {
usedCols.add(x);
}
for (y = gd.gridy; y < gd.gridy + gd.gridh; y++) {
usedRows.add(y);
}
}
let maxCol = usedCols.last();
for (x = maxCol; x >= 0; x--) {
if (!usedCols.contains(x)) {
// eliminate column
// ticket 86645 use member gridDatas instead of param cons
for (i = 0; i < this.gridDatas.length; i++) {
gd = this.gridDatas[i];
if (gd.gridx > x) {
gd.gridx--;
}
}
}
}
let maxRow = usedRows.last();
for (y = maxRow; y >= 0; y--) {
if (!usedRows.contains(y)) {
// eliminate row
// ticket 86645 use member gridDatas instead of param cons
for (i = 0; i < this.gridDatas.length; i++) {
gd = this.gridDatas[i];
if (gd.gridy > y) {
// ticket 86645
gd.gridy--;
}
}
}
}
this.cols = usedCols.size();
this.rows = usedRows.size();
$.log.isTraceEnabled() && $.log.trace('(LogicalGridLayoutInfo#CTOR) $components.length=' + this.$components.length + ' usedCols=' + this.cols + ' usedRows=' + this.rows);
this._initializeInfo();
}
protected _initializeInfo() {
let compCount = this.$components.length;
let uiHeightElements = [];
for (let i = 0; i < compCount; i++) {
// cleanup constraints
let $comp = this.$components[i];
let cons = this.gridDatas[i];
if (cons.gridx < 0) {
cons.gridx = 0;
}
if (cons.gridy < 0) {
cons.gridy = 0;
}
if (cons.gridw < 1) {
cons.gridw = 1;
}
if (cons.gridh < 1) {
cons.gridh = 1;
}
if (cons.gridx >= this.cols) {
cons.gridx = this.cols - 1;
}
if (cons.gridy >= this.rows) {
cons.gridy = this.rows - 1;
}
if (cons.gridx + cons.gridw - 1 >= this.cols) {
cons.gridw = this.cols - cons.gridx;
}
if (cons.gridy + cons.gridh >= this.rows) {
cons.gridh = this.rows - cons.gridy;
}
// Calculate and cache component size
let size = new Dimension(0, 0);
if (cons.widthHint > 0) {
// Use explicit width hint, if set
size.width = cons.widthHint;
// eslint-disable-next-line brace-style
} else if (cons.useUiWidth || !cons.fillHorizontal) {
// Calculate preferred width otherwise
// This size is needed by _initializeColumns
// But only if really needed by the logical grid layout (because it is expensive)
size = this.uiSizeInPixel($comp);
}
if (cons.heightHint > 0) {
// Use explicit height hint, if set
size.height = cons.heightHint;
} else if (cons.useUiHeight || !cons.fillVertical) {
// Otherwise check if preferred height should be calculated.
// Don't do it now because weightX need to be calculated first to get the correct width hints
uiHeightElements.push({
cons: cons,
$comp: $comp,
index: i
});
}
this.compSize[i] = size;
}
// Calculate this.width and this.weightX
this._initializeColumns();
if (this.widthOnly) {
// Abort here if only width is of interest
this.height = arrays.init(this.rows, [0, 0, 0]);
return;
}
// Calculate preferred heights using the width hints
if (this.widthHint && uiHeightElements.length > 0) {
let totalHGap = Math.max(0, (this.cols - 1) * this.hgap);
this.widthHints = this.layoutSizes(this.widthHint - totalHGap, this.width, this.weightX);
}
uiHeightElements.forEach(elem => {
let $comp = elem.$comp;
let cons = elem.cons;
let widthHint = this.widthHintForGridData(cons);
if (!cons.fillHorizontal) {
widthHint = Math.min(widthHint, this.compSize[elem.index].width);
}
this.compSize[elem.index] = this.uiSizeInPixel($comp, {
widthHint: widthHint
});
});
// Calculate this.height and this.weightY
this._initializeRows();
}
protected _initializeColumns() {
let compSize = this.compSize;
let compCount = compSize.length;
let prefWidths = arrays.init(this.cols, 0);
let maxWidths = arrays.init(this.cols, 10240);
let fixedWidths = arrays.init(this.cols, false);
for (let i = 0; i < compCount; i++) {
let cons = this.gridDatas[i];
if (cons.gridw === 1) {
let prefw;
if (cons.widthHint > 0) {
prefw = cons.widthHint;
} else if (cons.useUiWidth) {
prefw = compSize[i].width;
} else {
prefw = this.logicalWidthInPixel(cons);
}
prefw = Math.floor(prefw);
let x = cons.gridx;
if (x < this.cols) {
prefWidths[x] = Math.max(prefWidths[x], prefw);
maxWidths[x] = Math.min(maxWidths[x], cons.maxWidth);
if (cons.weightx === 0) {
fixedWidths[x] = true;
}
}
}
}
const lc = LayoutConstants;
for (let i = 0; i < compCount; i++) {
let cons = this.gridDatas[i];
if (cons.gridw > 1) {
let hSpan = cons.gridw;
let spanWidth = 0;
let distWidth;
for (let j = cons.gridx; j < cons.gridx + cons.gridw && j < this.cols; j++) {
if (!fixedWidths[j]) {
spanWidth += prefWidths[j];
}
}
if (cons.widthHint > 0) {
distWidth = cons.widthHint;
} else if (cons.useUiWidth) {
distWidth = compSize[i].width;
} else {
distWidth = this.logicalWidthInPixel(cons);
}
let hGaps = (hSpan - 1) * this.hgap;
distWidth -= hGaps;
if (distWidth > spanWidth) {
this._distributeWidth(cons, distWidth, prefWidths, fixedWidths, Math.max.bind(Math));
}
this._distributeWidth(cons, cons.maxWidth - hGaps, maxWidths, fixedWidths, Math.min.bind(Math));
}
}
for (let i = 0; i < this.cols; i++) {
this.width[i] = [];
if (fixedWidths[i]) {
this.width[i][lc.MIN] = prefWidths[i];
this.width[i][lc.PREF] = prefWidths[i];
this.width[i][lc.MAX] = Math.min(prefWidths[i], maxWidths[i]);
} else {
this.width[i][lc.MIN] = 0; // must be exactly 0!
this.width[i][lc.PREF] = prefWidths[i];
this.width[i][lc.MAX] = maxWidths[i];
}
}
// averaged column weights, normalized so that sum of weights is equal to
// 1.0
for (let i = 0; i < this.cols; i++) {
if (fixedWidths[i]) {
this.weightX[i] = 0;
} else {
let weightSum = 0;
let weightCount = 0;
for (let k = 0; k < compCount; k++) {
let cons = this.gridDatas[k];
if (cons.weightx > 0 && cons.gridx <= i && i <= cons.gridx + cons.gridw - 1) {
weightSum += (cons.weightx / cons.gridw);
weightCount++;
}
}
this.weightX[i] = (weightCount > 0 ? weightSum / weightCount : 0);
}
}
let sumWeightX = 0;
for (let i = 0; i < this.cols; i++) {
sumWeightX += this.weightX[i];
}
if (sumWeightX >= 1e-6) {
let f = 1.0 / sumWeightX;
for (let i = 0; i < this.cols; i++) {
this.weightX[i] = this.weightX[i] * f;
}
}
}
/**
* @param cons the current grid data
* @param distWidth the width to distribute to the columns. The width is distributed to the columns equally.
* @param widths the column widths that have been distributed so far when the previous rows were visited
* @param fixedWidths the columns with a fixed width (weightx = 0)
* @param calc a function that is called for each element of the given `widths` array, e.g. to decide whether to use the newly calculated column width or the existing one.
*/
protected _distributeWidth(cons: LogicalGridData, distWidth: number, widths: number[], fixedWidths: boolean[], calc: (equalWidth: number, width: number) => number) {
let hSpan = cons.gridw;
let equalWidth = Math.floor(distWidth / hSpan);
let remainder = distWidth % hSpan;
let last = -1;
for (let j = cons.gridx; j < cons.gridx + cons.gridw && j < this.cols; j++) {
last = j;
if (!fixedWidths[j]) {
widths[j] = calc(equalWidth, widths[j]);
}
if (cons.weightx === 0) {
fixedWidths[j] = true;
}
}
if (last > -1) {
widths[last] += remainder;
}
}
/**
* @param cons the current grid data
* @param distHeight the height to distribute to the rows. The height is distributed to the rows equally.
* @param widths the row heights that have been distributed so far when the previous column were visited
* @param fixedHeights the rows with a fixed height (weighty = 0)
* @param calc a function that is called for each element of the given `heights` array, e.g. to decide whether to use the newly calculated row height or the existing one.
*/
protected _distributeHeight(cons: LogicalGridData, distHeight: number, heights: number[], fixedHeights: boolean[], calc: (equalWidth: number, width: number) => number) {
let vSpan = cons.gridh;
let equalHeight = Math.floor(distHeight / vSpan);
let remainder = distHeight % vSpan;
let last = -1;
for (let j = cons.gridy; j < cons.gridy + cons.gridh && j < this.rows; j++) {
last = j;
if (!fixedHeights[j]) {
heights[j] = calc(equalHeight, heights[j]);
}
if (cons.weighty === 0) {
fixedHeights[j] = true;
}
}
if (last > -1) {
heights[last] += remainder;
}
}
protected _initializeRows() {
let compSize = this.compSize;
let compCount = compSize.length;
let prefHeights = arrays.init(this.rows, 0);
let maxHeights = arrays.init(this.rows, 10240);
let fixedHeights = arrays.init(this.rows, false);
for (let i = 0; i < compCount; i++) {
let cons = this.gridDatas[i];
if (cons.gridh === 1) {
let prefh;
if (cons.heightHint > 0) {
prefh = cons.heightHint;
} else if (cons.useUiHeight) {
prefh = compSize[i].height;
} else {
prefh = this.logicalHeightInPixel(cons);
}
prefh = Math.floor(prefh);
let y = cons.gridy;
if (y < this.rows) {
prefHeights[y] = Math.max(prefHeights[y], prefh);
maxHeights[y] = Math.min(maxHeights[y], cons.maxHeight);
if (cons.weighty === 0) {
fixedHeights[y] = true;
}
}
}
}
const lc = LayoutConstants;
for (let i = 0; i < compCount; i++) {
let cons = this.gridDatas[i];
if (cons.gridh > 1) {
let vSpan = cons.gridh;
let spanHeight = 0;
let distHeight;
for (let j = cons.gridy; j < cons.gridy + cons.gridh && j < this.rows; j++) {
if (!fixedHeights[j]) {
spanHeight += prefHeights[j];
}
}
let vGaps = (vSpan - 1) * this.vgap;
if (cons.heightHint > 0) {
distHeight = cons.heightHint;
} else if (cons.useUiHeight) {
distHeight = compSize[i].height;
} else {
distHeight = this.logicalHeightInPixel(cons);
}
distHeight -= vGaps;
if (distHeight > spanHeight) {
this._distributeHeight(cons, distHeight, prefHeights, fixedHeights, Math.max.bind(this));
}
this._distributeHeight(cons, cons.maxHeight - vGaps, maxHeights, fixedHeights, Math.min.bind(this));
}
}
for (let i = 0; i < this.rows; i++) {
this.height[i] = [];
if (fixedHeights[i]) {
this.height[i][lc.MIN] = prefHeights[i];
this.height[i][lc.PREF] = prefHeights[i];
this.height[i][lc.MAX] = Math.min(prefHeights[i], maxHeights[i]);
} else {
this.height[i][lc.MIN] = 0; // must be exactly 0!
this.height[i][lc.PREF] = prefHeights[i];
this.height[i][lc.MAX] = maxHeights[i];
}
}
// averaged row weights, normalized so that sum of weights is equal to 1.0
for (let i = 0; i < this.rows; i++) {
if (fixedHeights[i]) {
this.weightY[i] = 0;
} else {
let weightSum = 0;
let weightCount = 0;
for (let k = 0; k < compCount; k++) {
let cons = this.gridDatas[k];
if (cons.weighty > 0 && cons.gridy <= i && i <= cons.gridy + cons.gridh - 1) {
weightSum += (cons.weighty / cons.gridh);
weightCount++;
}
}
this.weightY[i] = (weightCount > 0 ? weightSum / weightCount : 0);
}
}
let sumWeightY = 0;
for (let i = 0; i < this.rows; i++) {
sumWeightY += this.weightY[i];
}
if (sumWeightY >= 1e-6) {
let f = 1.0 / sumWeightY;
for (let i = 0; i < this.rows; i++) {
this.weightY[i] = this.weightY[i] * f;
}
}
}
layoutCellBounds(size: Dimension, insets: Insets): Rectangle[][] {
let w = this.layoutSizes(size.width - insets.horizontal() - Math.max(0, (this.cols - 1) * this.hgap), this.width, this.weightX);
let h = this.layoutSizes(size.height - insets.vertical() - Math.max(0, (this.rows - 1) * this.vgap), this.height, this.weightY);
this.cellBounds = arrays.init(this.rows, null);
let y = insets.top;
for (let r = 0; r < this.rows; r++) {
let x = insets.left;
this.cellBounds[r] = arrays.init(this.cols, null);
for (let c = 0; c < this.cols; c++) {
this.cellBounds[r][c] = new Rectangle(x, y, w[c], h[r]);
x += w[c];
x += this.hgap;
}
y += h[r];
y += this.vgap;
}
return this.cellBounds;
}
layoutSizes(targetSize: number, sizes: number[][], weights: number[]): number[] {
let outSizes = arrays.init(sizes.length, 0);
if (targetSize <= 0) {
for (let i = 0; i < sizes.length; i++) {
outSizes[i] = sizes[i][LayoutConstants.MIN];
}
return outSizes;
}
let sumSize = 0;
let tmpWeight = arrays.init(weights.length, 0.0);
let sumWeight = 0;
for (let i = 0; i < sizes.length; i++) {
outSizes[i] = Math.min(Math.max(sizes[i][LayoutConstants.PREF], sizes[i][LayoutConstants.MIN]), sizes[i][LayoutConstants.MAX]);
sumSize += outSizes[i];
tmpWeight[i] = weights[i];
/**
* autocorrection: if weight is 0 and min / max sizes are NOT equal then
* set weight to 1; if weight<eps set it to 0
*/
if (tmpWeight[i] < LayoutConstants.EPS) {
if (sizes[i][LayoutConstants.MAX] > sizes[i][LayoutConstants.MIN]) {
tmpWeight[i] = 1;
} else {
tmpWeight[i] = 0;
}
}
sumWeight += tmpWeight[i];
}
// normalize weights
if (sumWeight > 0) {
for (let i = 0; i < tmpWeight.length; i++) {
tmpWeight[i] = tmpWeight[i] / sumWeight;
}
}
let deltaInt = targetSize - sumSize;
// expand or shrink
if (Math.abs(deltaInt) > 0) {
// setup accumulators
/* float[] */
let accWeight = arrays.init(tmpWeight.length, 0.0);
let hasTargets;
if (deltaInt > 0) {
// expand, if delta is > 0
hasTargets = true;
while (deltaInt > 0 && hasTargets) {
hasTargets = false;
for (let i = 0; i < outSizes.length && deltaInt > 0; i++) {
if (tmpWeight[i] > 0 && outSizes[i] < sizes[i][LayoutConstants.MAX]) {
hasTargets = true;
accWeight[i] += tmpWeight[i];
if (accWeight[i] > 0) {
accWeight[i] -= 1;
outSizes[i] += 1;
deltaInt -= 1;
}
}
}
}
} else {
// shrink, if delta is <= 0
hasTargets = true;
while (deltaInt < 0 && hasTargets) {
hasTargets = false;
for (let i = 0; i < outSizes.length && deltaInt < 0; i++) {
if (tmpWeight[i] > 0 && outSizes[i] > sizes[i][LayoutConstants.MIN]) {
hasTargets = true;
accWeight[i] += tmpWeight[i];
if (accWeight[i] > 0) {
accWeight[i] -= 1;
outSizes[i] -= 1;
deltaInt += 1;
}
}
}
}
}
}
return outSizes;
}
logicalWidthInPixel(cons: LogicalGridData): number {
let gridW = cons.gridw;
return (this.columnWidth * gridW) + (this.hgap * Math.max(0, gridW - 1));
}
logicalHeightInPixel(cons: LogicalGridData): number {
let gridH = cons.gridh;
let addition = cons.logicalRowHeightAddition || 0;
return (this.rowHeight * gridH) + (this.vgap * Math.max(0, gridH - 1)) + addition;
}
uiSizeInPixel($comp: JQuery, options?: HtmlCompPrefSizeOptions): Dimension {
let htmlComp = HtmlComponent.get($comp);
return htmlComp.prefSize(options).add(htmlComp.margins());
}
/**
* @returns the width hint for the given gridData
*/
widthHintForGridData(gridData: LogicalGridData): number | null {
if (this.widthHints.length === 0) {
return null;
}
let widthHint = (gridData.gridw - 1) * this.hgap;
for (let i = gridData.gridx; i < gridData.gridx + gridData.gridw; i++) {
widthHint += this.widthHints[i];
}
return widthHint;
}
}