/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* 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 com.badlogic.gdx.scenes.scene2d.ui;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Touchable;
import com.badlogic.gdx.scenes.scene2d.utils.Layout;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.SnapshotArray;
/** A group that lays out its children side by side horizontally, with optional wrapping. This can be easier than using
* {@link Table} when actors need to be inserted into or removed from the middle of the group.
* <p>
* The preferred width is the sum of the children's preferred widths plus spacing. The preferred height is the largest preferred
* height of any child. The preferred size is slightly different when {@link #wrap() wrap} is enabled. The min size is the
* preferred size and the max size is 0.
* <p>
* Widgets are sized using their {@link Layout#getPrefWidth() preferred width}, so widgets which return 0 as their preferred width
* will be given a width of 0 (eg, a label with {@link Label#setWrap(boolean) word wrap} enabled).
* @author Nathan Sweet */
public class HorizontalGroup extends WidgetGroup {
private float prefWidth, prefHeight, lastPrefHeight;
private boolean sizeInvalid = true;
private FloatArray rowSizes; // row width, row height, ...
private int align = Align.left, rowAlign;
private boolean reverse, round = true, wrap, expand;
private float space, wrapSpace, fill, padTop, padLeft, padBottom, padRight;
public HorizontalGroup () {
setTouchable(Touchable.childrenOnly);
}
public void invalidate () {
super.invalidate();
sizeInvalid = true;
}
private void computeSize () {
sizeInvalid = false;
SnapshotArray<Actor> children = getChildren();
int n = children.size;
prefHeight = 0;
if (wrap) {
prefWidth = 0;
if (rowSizes == null)
rowSizes = new FloatArray();
else
rowSizes.clear();
FloatArray rowSizes = this.rowSizes;
float space = this.space, wrapSpace = this.wrapSpace;
float pad = padLeft + padRight, groupWidth = getWidth() - pad, x = 0, y = 0, rowHeight = 0;
int i = 0, incr = 1;
if (reverse) {
i = n - 1;
n = -1;
incr = -1;
}
for (; i != n; i += incr) {
Actor child = children.get(i);
float width, height;
if (child instanceof Layout) {
Layout layout = (Layout)child;
width = layout.getPrefWidth();
height = layout.getPrefHeight();
} else {
width = child.getWidth();
height = child.getHeight();
}
float incrX = width + (x > 0 ? space : 0);
if (x + incrX > groupWidth && x > 0) {
rowSizes.add(x);
rowSizes.add(rowHeight);
prefWidth = Math.max(prefWidth, x + pad);
if (y > 0) y += wrapSpace;
y += rowHeight;
rowHeight = 0;
x = 0;
incrX = width;
}
x += incrX;
rowHeight = Math.max(rowHeight, height);
}
rowSizes.add(x);
rowSizes.add(rowHeight);
prefWidth = Math.max(prefWidth, x + pad);
if (y > 0) y += wrapSpace;
prefHeight = Math.max(prefHeight, y + rowHeight);
} else {
prefWidth = padLeft + padRight + space * (n - 1);
for (int i = 0; i < n; i++) {
Actor child = children.get(i);
if (child instanceof Layout) {
Layout layout = (Layout)child;
prefWidth += layout.getPrefWidth();
prefHeight = Math.max(prefHeight, layout.getPrefHeight());
} else {
prefWidth += child.getWidth();
prefHeight = Math.max(prefHeight, child.getHeight());
}
}
}
prefHeight += padTop + padBottom;
if (round) {
prefWidth = Math.round(prefWidth);
prefHeight = Math.round(prefHeight);
}
}
public void layout () {
if (sizeInvalid) computeSize();
if (wrap) {
layoutWrapped();
return;
}
boolean round = this.round;
int align = this.align;
float space = this.space, padBottom = this.padBottom, fill = this.fill;
float rowHeight = (expand ? getHeight() : prefHeight) - padTop - padBottom, x = padLeft;
if ((align & Align.right) != 0)
x += getWidth() - prefWidth;
else if ((align & Align.left) == 0) // center
x += (getWidth() - prefWidth) / 2;
float startY;
if ((align & Align.bottom) != 0)
startY = padBottom;
else if ((align & Align.top) != 0)
startY = getHeight() - padTop - rowHeight;
else
startY = padBottom + (getHeight() - padBottom - padTop - rowHeight) / 2;
align = rowAlign;
SnapshotArray<Actor> children = getChildren();
int i = 0, n = children.size, incr = 1;
if (reverse) {
i = n - 1;
n = -1;
incr = -1;
}
for (int r = 0; i != n; i += incr) {
Actor child = children.get(i);
float width, height;
Layout layout = null;
if (child instanceof Layout) {
layout = (Layout)child;
width = layout.getPrefWidth();
height = layout.getPrefHeight();
} else {
width = child.getWidth();
height = child.getHeight();
}
if (fill > 0) height = rowHeight * fill;
if (layout != null) {
height = Math.max(height, layout.getMinHeight());
float maxHeight = layout.getMaxHeight();
if (maxHeight > 0 && height > maxHeight) height = maxHeight;
}
float y = startY;
if ((align & Align.top) != 0)
y += rowHeight - height;
else if ((align & Align.bottom) == 0) // center
y += (rowHeight - height) / 2;
if (round)
child.setBounds(Math.round(x), Math.round(y), Math.round(width), Math.round(height));
else
child.setBounds(x, y, width, height);
x += width + space;
if (layout != null) layout.validate();
}
}
private void layoutWrapped () {
float prefHeight = getPrefHeight();
if (prefHeight != lastPrefHeight) {
lastPrefHeight = prefHeight;
invalidateHierarchy();
}
int align = this.align;
boolean round = this.round;
float space = this.space, padBottom = this.padBottom, fill = this.fill, wrapSpace = this.wrapSpace;
float maxWidth = prefWidth - padLeft - padRight;
float rowY = prefHeight - padTop, groupWidth = getWidth(), xStart = padLeft, x = 0, rowHeight = 0;
if ((align & Align.top) != 0)
rowY += getHeight() - prefHeight;
else if ((align & Align.bottom) == 0) // center
rowY += (getHeight() - prefHeight) / 2;
if ((align & Align.right) != 0)
xStart += groupWidth - prefWidth;
else if ((align & Align.left) == 0) // center
xStart += (groupWidth - prefWidth) / 2;
groupWidth -= padRight;
align = this.rowAlign;
FloatArray rowSizes = this.rowSizes;
SnapshotArray<Actor> children = getChildren();
int i = 0, n = children.size, incr = 1;
if (reverse) {
i = n - 1;
n = -1;
incr = -1;
}
for (int r = 0; i != n; i += incr) {
Actor child = children.get(i);
float width, height;
Layout layout = null;
if (child instanceof Layout) {
layout = (Layout)child;
width = layout.getPrefWidth();
height = layout.getPrefHeight();
} else {
width = child.getWidth();
height = child.getHeight();
}
if (x + width > groupWidth || r == 0) {
x = xStart;
if ((align & Align.right) != 0)
x += maxWidth - rowSizes.get(r);
else if ((align & Align.left) == 0) // center
x += (maxWidth - rowSizes.get(r)) / 2;
rowHeight = rowSizes.get(r + 1);
if (r > 0) rowY -= wrapSpace;
rowY -= rowHeight;
r += 2;
}
if (fill > 0) height = rowHeight * fill;
if (layout != null) {
height = Math.max(height, layout.getMinHeight());
float maxHeight = layout.getMaxHeight();
if (maxHeight > 0 && height > maxHeight) height = maxHeight;
}
float y = rowY;
if ((align & Align.top) != 0)
y += rowHeight - height;
else if ((align & Align.bottom) == 0) // center
y += (rowHeight - height) / 2;
if (round)
child.setBounds(Math.round(x), Math.round(y), Math.round(width), Math.round(height));
else
child.setBounds(x, y, width, height);
x += width + space;
if (layout != null) layout.validate();
}
}
public float getPrefWidth () {
if (wrap) return 0;
if (sizeInvalid) computeSize();
return prefWidth;
}
public float getPrefHeight () {
if (sizeInvalid) computeSize();
return prefHeight;
}
/** If true (the default), positions and sizes are rounded to integers. */
public void setRound (boolean round) {
this.round = round;
}
/** The children will be displayed last to first. */
public HorizontalGroup reverse () {
this.reverse = true;
return this;
}
/** If true, the children will be displayed last to first. */
public HorizontalGroup reverse (boolean reverse) {
this.reverse = reverse;
return this;
}
public boolean getReverse () {
return reverse;
}
/** Sets the horizontal space between children. */
public HorizontalGroup space (float space) {
this.space = space;
return this;
}
public float getSpace () {
return space;
}
/** Sets the vertical space between rows when wrap is enabled. */
public HorizontalGroup wrapSpace (float wrapSpace) {
this.wrapSpace = wrapSpace;
return this;
}
public float getWrapSpace () {
return wrapSpace;
}
/** Sets the padTop, padLeft, padBottom, and padRight to the specified value. */
public HorizontalGroup pad (float pad) {
padTop = pad;
padLeft = pad;
padBottom = pad;
padRight = pad;
return this;
}
public HorizontalGroup pad (float top, float left, float bottom, float right) {
padTop = top;
padLeft = left;
padBottom = bottom;
padRight = right;
return this;
}
public HorizontalGroup padTop (float padTop) {
this.padTop = padTop;
return this;
}
public HorizontalGroup padLeft (float padLeft) {
this.padLeft = padLeft;
return this;
}
public HorizontalGroup padBottom (float padBottom) {
this.padBottom = padBottom;
return this;
}
public HorizontalGroup padRight (float padRight) {
this.padRight = padRight;
return this;
}
public float getPadTop () {
return padTop;
}
public float getPadLeft () {
return padLeft;
}
public float getPadBottom () {
return padBottom;
}
public float getPadRight () {
return padRight;
}
/** Sets the alignment of all widgets within the horizontal group. Set to {@link Align#center}, {@link Align#top},
* {@link Align#bottom}, {@link Align#left}, {@link Align#right}, or any combination of those. */
public HorizontalGroup align (int align) {
this.align = align;
return this;
}
/** Sets the alignment of all widgets within the horizontal group to {@link Align#center}. This clears any other alignment. */
public HorizontalGroup center () {
align = Align.center;
return this;
}
/** Sets {@link Align#top} and clears {@link Align#bottom} for the alignment of all widgets within the horizontal group. */
public HorizontalGroup top () {
align |= Align.top;
align &= ~Align.bottom;
return this;
}
/** Adds {@link Align#left} and clears {@link Align#right} for the alignment of all widgets within the horizontal group. */
public HorizontalGroup left () {
align |= Align.left;
align &= ~Align.right;
return this;
}
/** Sets {@link Align#bottom} and clears {@link Align#top} for the alignment of all widgets within the horizontal group. */
public HorizontalGroup bottom () {
align |= Align.bottom;
align &= ~Align.top;
return this;
}
/** Adds {@link Align#right} and clears {@link Align#left} for the alignment of all widgets within the horizontal group. */
public HorizontalGroup right () {
align |= Align.right;
align &= ~Align.left;
return this;
}
public int getAlign () {
return align;
}
public HorizontalGroup fill () {
fill = 1f;
return this;
}
/** @param fill 0 will use preferred width. */
public HorizontalGroup fill (float fill) {
this.fill = fill;
return this;
}
public float getFill () {
return fill;
}
public HorizontalGroup expand () {
expand = true;
return this;
}
/** When true and wrap is false, the rows will take up the entire horizontal group height. */
public HorizontalGroup expand (boolean expand) {
this.expand = expand;
return this;
}
public boolean getExpand () {
return expand;
}
/** Sets fill to 1 and expand to true. */
public HorizontalGroup grow () {
expand = true;
fill = 1;
return this;
}
/** If false, the widgets are arranged in a single row and the preferred width is the widget widths plus spacing. If true, the
* widgets will wrap using the width of the horizontal group. The preferred width of the group will be 0 as it is expected that
* something external will set the width of the group. Default is false.
* <p>
* When wrap is enabled, the group's preferred height depends on the width of the group. In some cases the parent of the group
* will need to layout twice: once to set the width of the group and a second time to adjust to the group's new preferred
* height. */
public HorizontalGroup wrap () {
wrap = true;
return this;
}
public HorizontalGroup wrap (boolean wrap) {
this.wrap = wrap;
return this;
}
public boolean getWrap () {
return wrap;
}
/** Sets the alignment of widgets within each row of the horizontal group. Set to {@link Align#center}, {@link Align#top}, or
* {@link Align#bottom}. */
public HorizontalGroup rowAlign (int row) {
this.rowAlign = row;
return this;
}
/** Sets the alignment of widgets within each row to {@link Align#center}. This clears any other alignment. */
public HorizontalGroup rowCenter () {
rowAlign = Align.center;
return this;
}
/** Sets {@link Align#top} and clears {@link Align#bottom} for the alignment of widgets within each row. */
public HorizontalGroup rowTop () {
rowAlign |= Align.top;
rowAlign &= ~Align.bottom;
return this;
}
/** Sets {@link Align#bottom} and clears {@link Align#top} for the alignment of widgets within each row. */
public HorizontalGroup rowBottom () {
rowAlign |= Align.bottom;
rowAlign &= ~Align.top;
return this;
}
protected void drawDebugBounds (ShapeRenderer shapes) {
super.drawDebugBounds(shapes);
if (!getDebug()) return;
shapes.set(ShapeType.Line);
shapes.setColor(getStage().getDebugColor());
shapes.rect(getX() + padLeft, getY() + padBottom, getOriginX(), getOriginY(), getWidth() - padLeft - padRight,
getHeight() - padBottom - padTop, getScaleX(), getScaleY(), getRotation());
}
}