/* * Copyright 2014 MovingBlocks * * 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. */ package org.terasology.rendering.nui.layouts; import com.google.common.collect.Lists; import com.google.gson.annotations.SerializedName; import org.terasology.input.events.MouseButtonEvent; import org.terasology.input.events.MouseWheelEvent; import org.terasology.math.geom.Rect2i; import org.terasology.math.TeraMath; import org.terasology.math.geom.Vector2i; import org.terasology.rendering.nui.Canvas; import org.terasology.rendering.nui.CoreLayout; import org.terasology.rendering.nui.LayoutConfig; import org.terasology.rendering.nui.LayoutHint; import org.terasology.rendering.nui.UIWidget; import org.terasology.rendering.nui.events.NUIKeyEvent; import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; /** */ public class ColumnLayout extends CoreLayout<LayoutHint> { @LayoutConfig private int columns = 1; @LayoutConfig private int horizontalSpacing; @LayoutConfig private int verticalSpacing; @LayoutConfig private boolean autoSizeColumns; @LayoutConfig private boolean fillVerticalSpace = true; @LayoutConfig private boolean extendLast; private List<UIWidget> widgetList = Lists.newArrayList(); @LayoutConfig @SerializedName("column-widths") private float[] columnWidths = new float[]{1.0f}; public ColumnLayout() { } public ColumnLayout(String id) { super(id); } public void addWidget(UIWidget widget) { widgetList.add(widget); } @Override public void removeWidget(UIWidget widget) { widgetList.remove(widget); } public int getColumns() { return columns; } public void setColumns(int columns) { this.columns = columns; columnWidths = new float[columns]; float equalWidth = 1.0f / columns; for (int i = 0; i < columnWidths.length; ++i) { columnWidths[i] = equalWidth; } } public void setColumnWidths(float... widths) { if (widths.length > columns) { throw new IllegalArgumentException("More widths than columns"); } float total = 0; int columnIndex = 0; while (columnIndex < widths.length) { total += widths[columnIndex]; columnWidths[columnIndex] = widths[columnIndex]; columnIndex++; } if (total > 1.0f) { throw new IllegalArgumentException("Total width exceeds 1.0"); } if (columnIndex < columnWidths.length) { float remainingWidth = 1.0f - total; float widthPerColumn = remainingWidth / (columnWidths.length - columnIndex); while (columnIndex < columnWidths.length) { columnWidths[columnIndex++] = widthPerColumn; } } } @Override public void onDraw(Canvas canvas) { if (!widgetList.isEmpty()) { Vector2i availableSize = canvas.size(); int numRows = TeraMath.ceilToInt((float) widgetList.size() / columns); if (numRows > 0) { availableSize.y -= verticalSpacing * (numRows - 1); } if (columns > 0) { availableSize.x -= horizontalSpacing * (columns - 1); } List<List<UIWidget>> rows = Lists.newArrayList(getRowIterator()); List<RowInfo> rowInfos = Lists.newArrayList(); rowInfos.addAll(rows.stream().map(row -> calculateRowSize(row, canvas, availableSize)).collect(Collectors.toList())); int[] minWidths = new int[columns]; int minRowWidth = 0; int rowOffsetX = 0; if (autoSizeColumns) { for (RowInfo row : rowInfos) { for (int column = 0; column < row.widgetSizes.size(); column++) { minWidths[column] = Math.max(minWidths[column], row.widgetSizes.get(column).getX()); } } for (int width : minWidths) { minRowWidth += width; } minRowWidth += (columns - 1) * horizontalSpacing; rowOffsetX = (canvas.size().x - minRowWidth) / 2; } else { minRowWidth = canvas.size().x; for (int i = 0; i < columns; ++i) { minWidths[i] = TeraMath.floorToInt((minRowWidth - (columns - 1) * horizontalSpacing) * columnWidths[i]); } } int rowOffsetY = 0; int usedHeight = 0; for (RowInfo row : rowInfos) { usedHeight += row.height; } usedHeight += (rowInfos.size() - 1) * verticalSpacing; int excessHeight = canvas.size().y - usedHeight; if (fillVerticalSpace) { if (extendLast && numRows > 0) { // give all the extra space to the last entry rowInfos.get(numRows - 1).height += excessHeight; } else { // distribute extra height equally int extraSpacePerRow = excessHeight / rowInfos.size(); for (RowInfo row : rowInfos) { row.height += extraSpacePerRow; } } } else { rowOffsetY = excessHeight / 2; } for (int rowIndex = 0; rowIndex < rows.size(); ++rowIndex) { List<UIWidget> row = rows.get(rowIndex); RowInfo rowInfo = rowInfos.get(rowIndex); int cellOffsetX = rowOffsetX; for (int i = 0; i < row.size(); ++i) { UIWidget widget = row.get(i); int rowHeight = rowInfo.height; if (widget != null) { Rect2i drawRegion = Rect2i.createFromMinAndSize(cellOffsetX, rowOffsetY, minWidths[i], rowHeight); canvas.drawWidget(widget, drawRegion); } cellOffsetX += minWidths[i] + horizontalSpacing; } rowOffsetY += rowInfo.height + verticalSpacing; } } } private RowInfo calculateRowSize(List<UIWidget> row, Canvas canvas, Vector2i areaHint) { int availableWidth = areaHint.x - horizontalSpacing * (columns - 1); RowInfo rowInfo = new RowInfo(); for (int i = 0; i < columns && i < row.size(); ++i) { UIWidget widget = row.get(i); Vector2i cellSize = new Vector2i(availableWidth, areaHint.y); if (!autoSizeColumns) { cellSize.x *= columnWidths[i]; } if (widget != null) { Vector2i contentSize = canvas.calculateRestrictedSize(widget, cellSize); rowInfo.widgetSizes.add(contentSize); rowInfo.height = Math.max(rowInfo.height, contentSize.y); } else { rowInfo.widgetSizes.add(new Vector2i(0, 0)); } } return rowInfo; } @Override public Vector2i getPreferredContentSize(Canvas canvas, Vector2i areaHint) { Vector2i availableSize = new Vector2i(areaHint); int numRows = TeraMath.ceilToInt((float) widgetList.size() / columns); if (numRows > 0) { availableSize.y -= verticalSpacing * (numRows - 1); } if (columns > 0) { availableSize.x -= horizontalSpacing * (columns - 1); } Iterator<List<UIWidget>> rows = getRowIterator(); Vector2i size = new Vector2i(); int[] columnSizes = new int[columns]; while (rows.hasNext()) { List<UIWidget> row = rows.next(); RowInfo rowInfo = calculateRowSize(row, canvas, availableSize); size.y += rowInfo.height; if (rows.hasNext()) { size.y += verticalSpacing; } for (int i = 0; i < rowInfo.widgetSizes.size(); ++i) { columnSizes[i] = Math.max(columnSizes[i], rowInfo.widgetSizes.get(i).getX()); } } for (int columnSize : columnSizes) { size.x += columnSize; } if (!autoSizeColumns) { for (int i = 0; i < columns; ++i) { size.x = Math.max(size.x, TeraMath.floorToInt(columnSizes[i] / columnWidths[i])); } } size.x += horizontalSpacing * (columns - 1); return size; } @Override public Vector2i getMaxContentSize(Canvas canvas) { Iterator<List<UIWidget>> rows = getRowIterator(); Vector2i size = new Vector2i(); int[] columnSizes = new int[columns]; while (rows.hasNext()) { List<UIWidget> row = rows.next(); int rowHeight = 0; for (int i = 0; i < row.size(); ++i) { Vector2i maxSize = canvas.calculateMaximumSize(row.get(i)); columnSizes[i] = Math.max(columnSizes[i], maxSize.x); rowHeight = Math.max(rowHeight, maxSize.y); } size.y = TeraMath.addClampAtMax(size.y, rowHeight); if (rows.hasNext()) { size.y = TeraMath.addClampAtMax(size.y, verticalSpacing); } } long width = 0; for (int columnSize : columnSizes) { width += columnSize; } if (!autoSizeColumns) { for (int i = 0; i < columns; ++i) { width = Math.min(width, TeraMath.floorToInt(columnSizes[i] / columnWidths[i])); } } width += horizontalSpacing * (columns - 1); size.x = (int) Math.min(Integer.MAX_VALUE, width); return size; } @Override public void update(float delta) { for (UIWidget widget : widgetList) { widget.update(delta); } } @Override public void onMouseButtonEvent(MouseButtonEvent event) { } @Override public void onMouseWheelEvent(MouseWheelEvent event) { } @Override public boolean onKeyEvent(NUIKeyEvent event) { return false; } @Override public Iterator<UIWidget> iterator() { return widgetList.iterator(); } @Override public void addWidget(UIWidget element, LayoutHint hint) { addWidget(element); } public int getHorizontalSpacing() { return horizontalSpacing; } public void setHorizontalSpacing(int horizontalSpacing) { this.horizontalSpacing = horizontalSpacing; } public int getVerticalSpacing() { return verticalSpacing; } public void setVerticalSpacing(int verticalSpacing) { this.verticalSpacing = verticalSpacing; } public boolean isAutoSizeColumns() { return autoSizeColumns; } public boolean isFillVerticalSpace() { return fillVerticalSpace; } /** * @param fillVerticalSpace true if the vertical space of the canvas should be filled. * The elements are centered vertically otherwise. */ public void setFillVerticalSpace(boolean fillVerticalSpace) { this.fillVerticalSpace = fillVerticalSpace; } public void setAutoSizeColumns(boolean autoSizeColumns) { this.autoSizeColumns = autoSizeColumns; } private Iterator<List<UIWidget>> getRowIterator() { return new Iterator<List<UIWidget>>() { Iterator<UIWidget> contentIterator = iterator(); @Override public boolean hasNext() { return contentIterator.hasNext(); } @Override public List<UIWidget> next() { List<UIWidget> row = Lists.newArrayList(); for (int i = 0; i < columns; ++i) { if (contentIterator.hasNext()) { row.add(contentIterator.next()); } } return row; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } private static class RowInfo { private int height; private List<Vector2i> widgetSizes = Lists.newArrayList(); @Override public String toString() { return super.toString() + "{height:" + height + ", widgetSizes:" + widgetSizes + "}"; } } }