001package squidpony.squidgrid.gui.gdx; 002 003import java.util.LinkedList; 004import java.util.ListIterator; 005 006import com.badlogic.gdx.graphics.Color; 007import com.badlogic.gdx.graphics.g2d.Batch; 008import com.badlogic.gdx.graphics.g2d.BitmapFont; 009import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData; 010import com.badlogic.gdx.graphics.g2d.BitmapFontCache; 011import com.badlogic.gdx.graphics.g2d.GlyphLayout; 012import com.badlogic.gdx.graphics.glutils.ShapeRenderer; 013import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; 014import com.badlogic.gdx.math.MathUtils; 015import com.badlogic.gdx.scenes.scene2d.Actor; 016import com.badlogic.gdx.utils.Align; 017 018import squidpony.panel.IColoredString; 019import squidpony.panel.IMarkup; 020 021/** 022 * An actor capable of drawing {@link IColoredString}s. It is lines-oriented: 023 * putting a line may erase a line put before. It is designed to write text with 024 * a serif font (as opposed to {@link SquidPanel}). It performs line wrapping by 025 * default. It can write from top to bottom or from bottom to top (the default). 026 * 027 * <p> 028 * This 029 * <a href="https://twitter.com/hgamesdev/status/736091292132724736">tweet</a> 030 * shows an example. The panel at the top of the screenshot is implemented using 031 * this class (with {@link #drawBottomUp} being {@code true}). 032 * </p> 033 * 034 * <p> 035 * Contrary to {@link SquidMessageBox}, this panel doesn't support scrolling 036 * (for now). So it's suited when it is fine forgetting old messages (as in 037 * brogue's messages area). 038 * </p> 039 * 040 * @author smelC 041 * @param <T> 042 * 043 * @see SquidMessageBox An alternative, doing similar lines-drawing business, 044 * but being backed up by {@link SquidPanel}. 045 */ 046public class LinesPanel<T extends Color> extends Actor { 047 048 /** The markup used to typeset {@link #content}. */ 049 protected final IMarkup<T> markup; 050 051 /** The font used to draw {@link #content}. */ 052 protected final BitmapFont font; 053 054 /** What to display. Doesn't contain {@code null} entries. */ 055 protected final LinkedList<IColoredString<T>> content; 056 057 /** The maximal size of {@link #content} */ 058 protected final int maxLines; 059 060 /** 061 * The renderer used by {@link #clearArea(Batch)}. Do not access directly: 062 * use {@link #getRenderer()} instead. 063 */ 064 protected /* @Nullable */ ShapeRenderer renderer; 065 066 /** 067 * The horizontal offset to use when writing. If you aren't doing anything 068 * weird, should be left to {@code 0}. 069 */ 070 public float xOffset = 0; 071 072 /** 073 * The vertical offset to use when writing. If you aren't doing anything 074 * weird, should be left to {@code 0}. 075 */ 076 public float yOffset = 0; 077 078 /** 079 * If {@code true}, draws: 080 * 081 * <pre> 082 * ... 083 * content[1] 084 * content[0] 085 * </pre> 086 * 087 * If {@code false}, draws: 088 * 089 * <pre> 090 * content[0] 091 * content[1] 092 * ... 093 * </pre> 094 */ 095 public boolean drawBottomUp = false; 096 097 /** 098 * The color to use to clear the screen before drawing. Set it to 099 * {@code null} if you clean on your own. 100 */ 101 public Color clearingColor = Color.BLACK; 102 103 /* Now comes the usual libgdx options */ 104 105 /** Whether to wrap text */ 106 public boolean wrap = true; 107 108 /** The alignment used when typesetting */ 109 public int align = Align.left; 110 111 /** 112 * @param markup 113 * The markup to use, or {@code null} if none. You likely want to 114 * give {@link GDXMarkup}. If non-{@code null}, markup will be 115 * enabled in {@code font}. 116 * @param font 117 * The font to use. 118 * @param maxLines 119 * The maximum number of lines that this panel should display. 120 * Must be {@code >= 0}. 121 * @throws IllegalStateException 122 * If {@code maxLines < 0} 123 */ 124 public LinesPanel(/* @Nullable */ IMarkup<T> markup, BitmapFont font, int maxLines) { 125 this.markup = markup; 126 this.font = font; 127 if (markup != null) 128 this.font.getData().markupEnabled |= true; 129 this.content = new LinkedList<IColoredString<T>>(); 130 if (maxLines < 0) 131 throw new IllegalStateException("The maximum number of lines in an instance of " 132 + getClass().getSimpleName() + " must be greater or equal than zero"); 133 this.maxLines = maxLines; 134 } 135 136 /** 137 * @param font 138 * @param height 139 * @return The last argument to give to 140 * {@link #WrappingLinesPanel(IMarkup, BitmapFont, int)} when the 141 * desired <b>pixel</b> height is {@code height} 142 */ 143 public static int computeMaxLines(BitmapFont font, float height) { 144 return MathUtils.ceil(height / font.getData().lineHeight); 145 } 146 147 /** 148 * Adds {@code ics} first in {@code this}, possibly removing the last entry, 149 * if {@code this}' size would grow over {@link #maxLines}. 150 * 151 * @param ics 152 */ 153 public void addFirst(IColoredString<T> ics) { 154 if (ics == null) 155 throw new NullPointerException("Adding a null entry is forbidden"); 156 if (atMax()) 157 content.removeLast(); 158 content.addFirst(ics); 159 } 160 161 /** 162 * Adds {@code ics} last in {@code this}, possibly removing the last entry, 163 * if {@code this}' size would grow over {@link #maxLines}. 164 * 165 * @param ics 166 */ 167 public void addLast(IColoredString<T> ics) { 168 if (ics == null) 169 throw new NullPointerException("Adding a null entry is forbidden"); 170 if (atMax()) 171 content.removeLast(); 172 content.addLast(ics); 173 } 174 175 @Override 176 public void draw(Batch batch, float parentAlpha) { 177 clearArea(batch); 178 179 final float width = getWidth(); 180 181 final BitmapFontData data = font.getData(); 182 final float lineHeight = data.lineHeight; 183 184 final float height = getHeight(); 185 186 final float x = getX() + xOffset; 187 float y = getY() + (drawBottomUp ? lineHeight : height) - data.descent + yOffset; 188 189 final ListIterator<IColoredString<T>> it = content.listIterator(); 190 int ydx = 0; 191 float consumed = 0; 192 while (it.hasNext()) { 193 final IColoredString<T> ics = it.next(); 194 final String str = toDraw(ics, ydx); 195 /* Let's see if the drawing would go outside this Actor */ 196 final BitmapFontCache cache = font.getCache(); 197 cache.clear(); 198 final GlyphLayout glyph = cache.addText(str, 0, y, width, align, wrap); 199 if (height < consumed + glyph.height) 200 /* We would draw outside this Actor's bounds */ 201 break; 202 final int increaseAlready; 203 if (drawBottomUp) { 204 /* 205 * If the text span multiple lines and we draw bottom-up, we 206 * must go up *before* drawing. 207 */ 208 final int nbLines = MathUtils.ceil(glyph.height / lineHeight); 209 if (1 < nbLines) { 210 increaseAlready = nbLines - 1; 211 y += increaseAlready * lineHeight; 212 } else 213 increaseAlready = 0; 214 } else 215 increaseAlready = 0; 216 /* Actually draw */ 217 font.draw(batch, str, x, y, width, align, wrap); 218 y += (drawBottomUp ? /* Go up */ 1 : /* Go down */ -1) * glyph.height; 219 y -= increaseAlready * lineHeight; 220 consumed += glyph.height; 221 ydx++; 222 } 223 } 224 225 /** 226 * Paints this panel with {@link #clearingColor} 227 */ 228 protected void clearArea(Batch batch) { 229 if (clearingColor != null) { 230 batch.end(); 231 UIUtil.drawRectangle(getRenderer(), getX(), getY(), getWidth(), getHeight(), ShapeType.Filled, 232 clearingColor); 233 batch.begin(); 234 } 235 } 236 237 protected boolean atMax() { 238 return content.size() == maxLines; 239 240 } 241 242 protected String toDraw(IColoredString<T> ics, int ydx) { 243 return applyMarkup(transform(ics, ydx)); 244 } 245 246 protected String applyMarkup(IColoredString<T> ics) { 247 if (ics == null) 248 return null; 249 else 250 return markup == null ? ics.toString() : ics.presentWithMarkup(markup); 251 } 252 253 /** 254 * If you want to grey out "older" messages, you would do it in this method, 255 * when {@code ydx > 0} (using an {@link IColorCenter} maybe ?). 256 * 257 * @param ics 258 * @param ydx 259 * The index of {@link #ics} within {@link #content}. 260 * @return A variation of {@code ics}, or {@code ics} itself. 261 */ 262 protected IColoredString<T> transform(IColoredString<T> ics, int ydx) { 263 return ics; 264 } 265 266 protected ShapeRenderer getRenderer() { 267 if (renderer == null) 268 renderer = new ShapeRenderer(); 269 return renderer; 270 } 271 272}