001package squidpony.squidgrid.gui.gdx; 002 003import com.badlogic.gdx.graphics.Color; 004import com.badlogic.gdx.graphics.g2d.Batch; 005import com.badlogic.gdx.graphics.g2d.BitmapFont; 006import com.badlogic.gdx.graphics.g2d.BitmapFontCache; 007import com.badlogic.gdx.graphics.g2d.GlyphLayout; 008import com.badlogic.gdx.graphics.glutils.ShapeRenderer; 009import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; 010import com.badlogic.gdx.math.MathUtils; 011import com.badlogic.gdx.math.Matrix4; 012import com.badlogic.gdx.scenes.scene2d.Actor; 013import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; 014import com.badlogic.gdx.utils.Align; 015import squidpony.panel.IColoredString; 016import squidpony.squidgrid.gui.gdx.UIUtil.CornerStyle; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Collections; 021 022/** 023 * A panel to display some text using libgdx directly (i.e. without using 024 * {@link SquidPanel}) as in these examples (no scrolling first, then with a 025 * scroll bar): 026 * 027 * <p> 028 * <ul> 029 * <li><img src="http://i.imgur.com/EqEXqlu.png"/></li> 030 * <li><img src="http://i.imgur.com/LYbxQZE.png"/></li> 031 * </ul> 032 * </p> 033 * 034 * <p> 035 * It supports vertical scrolling, i.e. it'll put a vertical scrollbar if 036 * there's too much text to display. This class does a lot of stuff, you 037 * typically only have to provide the textures for the scrollbars and the scroll 038 * knobs (see example below). 039 * </p> 040 * 041 * <p> 042 * A typical usage of this class is as follows: 043 * 044 * <pre>{@code 045 * final TextPanel<Color> tp = new TextPanel<>(new GDXMarkup(), font); 046 * tp.init(screenWidth, screenHeight, text); <- first 2 params: for fullscreen 047 * final ScrollPane sp = tp.getScrollPane(); 048 * sp.setScrollPaneStyle(new ScrollPaneStyle(...)); <- set textures 049 * stage.addActor(sp); 050 * stage.setScrollFocus(sp); 051 * stage.draw(); 052 * }</pre> 053 * </p> 054 * 055 * <p> 056 * This class shares what {@link ScrollPane} does (knobs, handling of the wheel). 057 * </p> 058 * <p> 059 * Drawing the result of {@link #getScrollPane()} will set the shader of {@code batch} if using a distance field or MSDF 060 * font and the shader is currently not configured for such a font; it does not reset the shader to the default so that 061 * multiple Actors can all use the same shader and so specific extra glyphs or other items can be rendered after calling 062 * draw(). If you need to draw both a distance field font and full-color art, you should set the shader on the Batch to 063 * null when you want to draw full-color art, and end the Batch between drawing this object and the other art. 064 * </p> 065 * @author smelC 066 * 067 * @see ScrollPane A libGDX widget for general scrolling through only the visible part of a large widget 068 */ 069public class TextPanel { 070 071 /** 072 * The color to use to paint the background (outside buttons) using 073 * {@link ShapeRenderer}. Or {@code null} to disable background coloring. 074 */ 075 public /* @Nullable */ Color backgroundColor; 076 077 /** 078 * The color of the border around this panel, if any. If set, it'll be 079 * rendered using {@link ShapeRenderer} and {@link #borderStyle}. 080 */ 081 public /* @Nullable */ Color borderColor; 082 083 /** The size of the border, if any */ 084 public float borderSize; 085 086 public CornerStyle borderStyle = CornerStyle.ROUNDED; 087 088 protected BitmapFont font; 089 protected TextCellFactory tcf; 090 /** The text to display */ 091 public ArrayList<CharSequence> text; 092 093 protected final ScrollPane scrollPane; 094 095 /** 096 * The actor whose size is adjusted to the text. When scrolling is required, 097 * it is bigger than {@link #scrollPane}. 098 */ 099 protected final Actor textActor; 100 101 /** Do not access directly, use {@link #getRenderer()} */ 102 private /* @Nullable */ ShapeRenderer renderer; 103 104 /** 105 * The text to display MUST be set later on with 106 * {@link #init(float, float, Collection)}. 107 * 108 * @param font 109 * The font to use. It can be set later using 110 * {@link #setFont(BitmapFont)}, but it MUST be set before 111 * drawing this panel. 112 */ 113 public TextPanel(/* @Nullable */ BitmapFont font) { 114 if (font != null) 115 setFont(font); 116 textActor = new TextActor(); 117 118 this.scrollPane = new ScrollPane(textActor); 119 } 120 121 /** 122 * The text to display MUST be set later on with {@link #init(float, float, Collection)} (which can't be updated) or 123 * {@link #initShared(float, float, ArrayList)} (which reflects changes in the given ArrayList). 124 * 125 * @param font 126 * A TextCellFactory, typically holding a distance field font ("stretchable" or "crisp" in 127 * DefaultResources). This won't force glyphs into same-size cells, despite the name. 128 */ 129 public TextPanel(/* @Nullable */ TextCellFactory font) { 130 if (font != null) 131 { 132 tcf = font; 133 tcf.initBySize(); 134 this.font = tcf.font(); 135 this.font.getData().markupEnabled = true; 136 } 137 textActor = new TextActor(); 138 scrollPane = new ScrollPane(textActor); 139 } 140 141 /** 142 * Sets the font to use. This method should be called once before {@link #init(float, float, Collection)} if the 143 * font wasn't given at creation-time. 144 * 145 * @param font The font to use as a BitmapFont. 146 */ 147 public void setFont(BitmapFont font) { 148 font.getData().markupEnabled = true; 149 this.font = font; 150 tcf = new TextCellFactory().font(font).height(MathUtils.ceil(font.getLineHeight())) 151 .width(MathUtils.round(font.getSpaceXadvance())); 152 } 153 154 /** 155 * Sets the font to use. This method should be called once before {@link #init(float, float, Collection)} if the 156 * font wasn't given at creation-time. 157 * 158 * @param font The font to use as a TextCellFactory. 159 */ 160 public void setFont(TextCellFactory font) { 161 if (font != null) 162 { 163 tcf = font; 164 tcf.initBySize(); 165 this.font = tcf.font(); 166 this.font.getData().markupEnabled = true; 167 } 168 } 169 170 /** 171 * This method sets the sizes of {@link #scrollPane} and {@link #textActor}. 172 * This method MUST be called before rendering. 173 * 174 * @param maxHeight 175 * The maximum height that the scrollpane can take (equal or 176 * smaller than the height of the text actor). 177 * @param width 178 * The width of the scrollpane and the text actor. 179 * @param coloredText any Collection of IColoredString that use Color or a subclass as their color type 180 */ 181 public void init(float width, float maxHeight, Collection<? extends IColoredString<Color>> coloredText) { 182 if (tcf == null) 183 throw new NullPointerException( 184 "The font should be set before calling TextPanel.init()"); 185 186 this.text = new ArrayList<>(coloredText.size()); 187 for (IColoredString<Color> ics : coloredText) 188 text.add(ics.presentWithMarkup(GDXMarkup.instance)); 189 scrollPane.setWidth(width); 190 textActor.setWidth(width); 191 192 scrollPane.setHeight(maxHeight); 193 scrollPane.setActor(textActor); 194 scrollPane.layout(); 195 } 196 197 /** 198 * This method sets the sizes of {@link #scrollPane} and {@link #textActor}, and shares a direct reference to 199 * {@code text} so changes to that ArrayList will also be picked up here and rendered. 200 * This method MUST be called before rendering. 201 * 202 * @param maxHeight 203 * The maximum height that the scrollpane can take (equal or 204 * smaller than the height of the text actor). 205 * @param width 206 * The width of the scrollpane and the text actor. 207 * @param text an ArrayList of CharSequence that will be used directly by this TextPanel (changes 208 * to the ArrayList will show up in the TextPanel) 209 */ 210 public void initShared(float width, float maxHeight, ArrayList<CharSequence> text) { 211 this.text = text; 212 213 scrollPane.setWidth(width); 214 textActor.setWidth(width); 215 216 if (tcf == null) 217 throw new NullPointerException( 218 "The font should be set before calling TextPanel.init()"); 219 220 //prepareText(); 221// final boolean yscroll = maxHeight < textActor.getHeight(); 222 scrollPane.setHeight(maxHeight); 223 scrollPane.setActor(textActor); 224 //yScrollingCallback(yscroll); 225 scrollPane.layout(); 226 } 227 228 public void init(float width, float maxHeight, Color color, String... text) 229 { 230 if (tcf == null) 231 throw new NullPointerException( 232 "The font should be set before calling TextPanel.init()"); 233 234 this.text = new ArrayList<>(text.length); 235 Collections.addAll(this.text, text); 236 scrollPane.setWidth(width); 237 textActor.setWidth(width); 238 239 scrollPane.setHeight(maxHeight); 240 scrollPane.setActor(textActor); 241 scrollPane.layout(); 242 } 243 244 /** 245 * Draws the border. You have to call this method manually, because the 246 * border is outside the actor and hence should be drawn at the very end, 247 * otherwise it can get overwritten by UI elements. 248 * 249 * @param batch 250 */ 251 public void drawBorder(Batch batch) { 252 if (borderColor != null && 0 < borderSize) { 253 final boolean reset = batch.isDrawing(); 254 if (reset) 255 batch.end(); 256 257 final ShapeRenderer sr = getRenderer(); 258 final Matrix4 m = batch.getTransformMatrix(); 259 sr.setTransformMatrix(m); 260 sr.begin(ShapeType.Filled); 261 sr.setColor(borderColor); 262// prepareText(); 263 final float down = font.getData().down; 264 UIUtil.drawMarginsAround(sr, scrollPane.getX(), scrollPane.getY() + down * -0.5f + 4f, scrollPane.getWidth(), 265 scrollPane.getHeight() + down * -1.5f - 4f, borderSize * 2f, borderColor, borderStyle, 1f, 1f); 266 sr.end(); 267 268 if (reset) 269 batch.begin(); 270 } 271 } 272 273 /** 274 * @return The text to draw, without color information present in {@link #text}. 275 */ 276 public /* @Nullable */ ArrayList<String> getTypesetText() { 277 if (text == null) 278 return null; 279 final ArrayList<String> result = new ArrayList<>(); 280 for (CharSequence line : text) { 281 result.add(GDXMarkup.instance.removeMarkup(line).toString()); 282 } 283 return result; 284 } 285 286 /** 287 * Updates the text this will show based on the current contents of the ArrayList of IColoredString values that may 288 * be shared due to {@link #initShared(float, float, ArrayList)}, then resizes the {@link #getTextActor()} to fit 289 * the current text and lays out the {@link #getScrollPane()} to match. Called in the text actor's draw() method. 290 */ 291 protected void prepareText() { 292 if (text == null) 293 return; 294 final BitmapFontCache cache = font.getCache(); 295 //cache.clear(); 296 final float w = scrollPane.getWidth(); 297 float lineHeight = -font.getData().down, capHeight = font.getCapHeight(); 298 int lines = 1;//, ci = 0; 299 StringBuilder sb = new StringBuilder(256); 300 if(tcf.supportedStyles() <= 1) { 301 for (int m = 0, textSize = text.size(); m < textSize; m++) { 302 sb.append(GDXMarkup.instance.colorStringOnlyMarkup(text.get(m))).append('\n'); 303 } 304 } 305 else 306 { 307 for (int m = 0, textSize = text.size(); m < textSize; m++) { 308 sb.append(GDXMarkup.instance.colorStringMarkup(text.get(m))).append('\n'); 309 } 310 } 311 312 GlyphLayout layout = cache.setText(sb, 0, 0, w, Align.left, true); 313 lines += layout.height / capHeight; 314 315 ////TODO: BitmapFontCache.setColors(float, int, int) is broken in libGDX 1.9.10; find some workaround 316// for (int m = 0, textSize = text.size(); m < textSize; m++) { 317// IColoredString<Color> line = text.get(m); 318// ArrayList<IColoredString.Bucket<Color>> frags = line.getFragments(); 319// for (int i = 0; i < frags.size(); i++) { 320// final IColoredString.Bucket<Color> b = frags.get(i); 321//// Color c = b.getColor(); 322//// if(c != null) 323//// cache.setColors(c, ci, (ci += b.length())); 324//// else 325// ci += b.length(); 326// } 327// 328//// if(m + 1 < textSize) 329//// { 330// //cache.addText("\n", pos, (-lines) * totalTextHeight, w, Align.left, true); 331// //lines++; 332//// } 333// } 334 lineHeight *= lines; 335 if(lineHeight < 0) 336 lineHeight = 0; 337 textActor.setHeight(/* Entire height */ lineHeight); 338 scrollPane.layout(); 339 340 } 341 342 /** 343 * Scrolls the scroll pane this holds down by some number of rows (which may be fractional, and may be negative to 344 * scroll up). This is not a smooth scroll, and will not be animated. 345 * @param downDistance The distance in rows to scroll down, which can be negative to scroll up instead 346 */ 347 public void scroll(final float downDistance) 348 { 349 prepareText(); 350 scrollPane.setScrollY(scrollPane.getScrollY() + downDistance * tcf.actualCellHeight); 351 } 352 353 /** 354 * If the parameter is true, scrolls to the top of this scroll pane; otherwise scrolls to the bottom. This is not a 355 * smooth scroll, and will not be animated. 356 * @param goToTop If true, will scroll to the top edge; if false, will scroll to the bottom edge. 357 */ 358 public void scrollToEdge(final boolean goToTop) 359 { 360 prepareText(); 361 scrollPane.setScrollPercentY(goToTop ? 0f : 1f); 362 } 363 364 /** 365 * @return The {@link ScrollPane} containing {@link #getTextActor()}. 366 */ 367 public ScrollPane getScrollPane() { 368 return scrollPane; 369 } 370 371 /** 372 * @return The {@link Actor} where the text is drawn. It may be bigger than 373 * {@link #getScrollPane()}. 374 */ 375 public Actor getTextActor() { 376 return textActor; 377 } 378 379 /** 380 * @return The font used, if set, as a TextCellFactory (one is always created even if only given a BitmapFont). 381 */ 382 public /* @Nullable */ TextCellFactory getFont() { 383 return tcf; 384 } 385 386 public void dispose() { 387 if (renderer != null) 388 renderer.dispose(); 389 } 390 391 /** 392 * Callback done to do stuff according to whether y-scrolling is required 393 * 394 * @param required 395 * Whether y scrolling is required. 396 */ 397 protected void yScrollingCallback(boolean required) { 398 if (required) { 399 /* Disable borders, they don't mix well with scrollbars */ 400 borderSize = 0; 401 scrollPane.setFadeScrollBars(false); 402 scrollPane.setForceScroll(false, true); 403 } 404 } 405 406 /** 407 * @return A fresh renderer. 408 */ 409 protected ShapeRenderer buildRenderer() { 410 return new ShapeRenderer(); 411 } 412 413 /** 414 * @return The renderer to use. 415 */ 416 protected ShapeRenderer getRenderer() { 417 if (renderer == null) 418 renderer = buildRenderer(); 419 return renderer; 420 } 421 422 private class TextActor extends Actor 423 { 424 TextActor() 425 { 426 427 } 428 @Override 429 public void draw(Batch batch, float parentAlpha) { 430 prepareText(); 431 final float tx = 0f;//scrollPane.getX(); 432 final float ty = 0f;//scrollPane.getY(); 433 final float twidth = scrollPane.getWidth(); 434 final float theight = scrollPane.getHeight(); 435 436 if (backgroundColor != null) { 437 batch.setColor(backgroundColor); 438 batch.draw(tcf.getSolid(), tx, ty, twidth, theight); 439 batch.setColor(SColor.WHITE); 440 /* 441 batch.end(); 442 443 final Matrix4 m = batch.getTransformMatrix(); 444 final ShapeRenderer sr = getRenderer(); 445 sr.setTransformMatrix(m); 446 sr.begin(ShapeType.Filled); 447 sr.setColor(backgroundColor); 448 UIUtil.drawRectangle(renderer, tx, ty, twidth, theight, ShapeType.Filled, 449 backgroundColor); 450 sr.end(); 451 452 batch.begin(); 453 */ 454 } 455 456 if (font == null) 457 throw new NullPointerException( 458 "The font should be set when drawing a TextPanel's TextActor"); 459 if (text == null) 460 throw new NullPointerException( 461 "The font should be set when drawing a TextPanel's TextActor"); 462 if (tcf != null) { 463 tcf.configureShader(batch); 464 } 465// final float offY = 0;//(tcf != null) ? tcf.actualCellHeight * 0.5f : 0; 466 final BitmapFontCache cache = font.getCache(); 467 cache.setPosition(tx, scrollPane.getHeight() + scrollPane.getScrollY()); 468 cache.draw(batch); 469 } 470 } 471 472}