/* * 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.common.collect.Maps; import gnu.trove.list.TIntList; import gnu.trove.list.array.TIntArrayList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.UIWidget; import java.util.Iterator; import java.util.List; import java.util.Map; /** * A layout that places widgets in a row, with support for relative widths */ public class RowLayout extends CoreLayout<RowLayoutHint> { private static final Logger logger = LoggerFactory.getLogger(RowLayout.class); /** * A list of the widgets in this {@link RowLayout} */ private List<UIWidget> contents = Lists.newArrayList(); /** * Maps widgets to their layout hints */ private Map<UIWidget, RowLayoutHint> hints = Maps.newHashMap(); /** * The spacing between adjacent widgets, in pixels */ @LayoutConfig private int horizontalSpacing; /** * Creates an empty {@code RowLayout}. */ public RowLayout() { } /** * Creates an empty {@code RowLayout} with the given id. * * @param id The id assigned to this {@code RowLayout} */ public RowLayout(String id) { super(id); } /** * Creates a {@code RowLayout} containing the given widgets. * * @param widgets A variable number of {@link UIWidget}s to be added to this layout's widget list */ public RowLayout(UIWidget... widgets) { for (UIWidget widget : widgets) { addWidget(widget, null); } } /** * Adds the widget to this layout's widget list. * * @param widget The {@code UIWidget} to be added * @param hint An optional {@link RowLayoutHint} specifying how the widget should be drawn in this layout = * such as whether it has a relative width or whether it uses the content width */ @Override public void addWidget(UIWidget widget, RowLayoutHint hint) { contents.add(widget); if (hint != null) { hints.put(widget, hint); } } /** * Removes the widget from this layout's widget list. * * @param widget The {@code UIWidget} to be removed */ @Override public void removeWidget(UIWidget widget) { contents.remove(widget); hints.remove(widget); } /** * Draws the widgets contained in this layout's widget list, * according to the widths calculated in {@link #calcWidths(Canvas)}. * This is called every frame. * * @param canvas The {@link Canvas} on which this {@code RowLayout} is drawn */ @Override public void onDraw(Canvas canvas) { TIntList widths = calcWidths(canvas); if (!contents.isEmpty()) { int xOffset = 0; for (int i = 0; i < contents.size(); ++i) { int itemWidth = widths.get(i); Rect2i region = Rect2i.createFromMinAndSize(xOffset, 0, itemWidth, canvas.size().y); canvas.drawWidget(contents.get(i), region); xOffset += itemWidth; xOffset += horizontalSpacing; } } } /** * Calculates the widths of each of the widgets in this layout's widget list. * Widths are first calculated for widgets with a relative width specified, * followed by widgets which follow their content width. * The remaining width is then split equally among the remaining widgets. * * @param canvas The {@link Canvas} on which this {@code RowLayout} is drawn * @return A list of the widths of the widgets in this layout's widget list, in pixels */ private TIntList calcWidths(Canvas canvas) { TIntList results = new TIntArrayList(contents.size()); if (contents.size() > 0) { int width = canvas.size().x - horizontalSpacing * (contents.size() - 1); int totalWidthUsed = 0; int unprocessedWidgets = 0; for (UIWidget widget : contents) { RowLayoutHint hint = hints.get(widget); if (hint != null) { if (!hint.isUseContentWidth() && hint.getRelativeWidth() != 0) { int elementWidth = TeraMath.floorToInt(hint.getRelativeWidth() * width); results.add(elementWidth); totalWidthUsed += elementWidth; } else { results.add(0); unprocessedWidgets++; } } else { results.add(0); unprocessedWidgets++; } } if (unprocessedWidgets > 0) { int remainingWidthPerElement = (width - totalWidthUsed) / unprocessedWidgets; for (int i = 0; i < results.size(); ++i) { if (results.get(i) == 0) { RowLayoutHint hint = hints.get(contents.get(i)); if (hint != null) { if (hint.isUseContentWidth()) { Vector2i contentSize = contents.get(i).getPreferredContentSize(canvas, new Vector2i(remainingWidthPerElement, canvas.size().y)); results.set(i, contentSize.x); totalWidthUsed += contentSize.x; unprocessedWidgets--; } } } } } if (unprocessedWidgets > 0) { int remainingWidthPerElement = (width - totalWidthUsed) / unprocessedWidgets; for (int i = 0; i < results.size(); ++i) { if (results.get(i) == 0) { results.set(i, remainingWidthPerElement); } } } } return results; } /** * Retrieves the preferred content size of this {@code RowLayout}. * This is the minimum size this layout will take, given no space restrictions. * * @param canvas The {@code Canvas} on which this {@code RowLayout} is drawn * @param areaHint A {@link Vector2i} representing the space available for widgets to be drawn in this layout * @return A {@link Vector2i} representing the preferred content size of this {@code RowLayout} */ @Override public Vector2i getPreferredContentSize(Canvas canvas, Vector2i areaHint) { TIntList widths = calcWidths(canvas); Vector2i result = new Vector2i(areaHint.x, 0); for (int i = 0; i < contents.size(); ++i) { Vector2i widgetSize = canvas.calculateRestrictedSize(contents.get(i), new Vector2i(TeraMath.floorToInt(widths.get(i)), areaHint.y)); result.y = Math.max(result.y, widgetSize.y); } return result; } /** * Retrieves the maximum content size of this {@code RowLayout}. * * @param canvas The {@code Canvas} on which this {@code RowLayout} is drawn * @return A {@code Vector2i} representing the maximum content size of this {@code RowLayout} */ @Override public Vector2i getMaxContentSize(Canvas canvas) { return new Vector2i(Integer.MAX_VALUE, Integer.MAX_VALUE); } /** * Retrieves an {@link Iterator} containing this layout's widget list. * * @return An {@code Iterator} containing the list of {@code UIWidgets} */ @Override public Iterator<UIWidget> iterator() { return contents.iterator(); } /** * Sets the ratios of the widths of the widgets in this {@code RowLayout}. * * @param ratios The ratios of the widths, each corresponding to a separate widget, with a maximum total of 1 * @return This {@code RowLayout} */ public RowLayout setColumnRatios(float... ratios) { hints.clear(); for (int i = 0; i < ratios.length; ++i) { hints.put(contents.get(i), new RowLayoutHint(ratios[i])); } return this; } /** * Retrieves the spacing between adjacent widgets in this {@code RowLayout}. * * @return The spacing, in pixels */ public int getHorizontalSpacing() { return horizontalSpacing; } /** * Sets the spacing betweeen adjacent widgets in this {@code RowLayout}. * * @param spacing The spacing, in pixels * @return This {@code RowLayout} */ public RowLayout setHorizontalSpacing(int spacing) { this.horizontalSpacing = spacing; return this; } }