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.InputEvent; 014import com.badlogic.gdx.scenes.scene2d.InputListener; 015import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; 016import com.badlogic.gdx.utils.Align; 017import squidpony.panel.IColoredString; 018import squidpony.panel.IMarkup; 019import squidpony.squidgrid.gui.gdx.UIUtil.CornerStyle; 020import squidpony.squidgrid.gui.gdx.UIUtil.YMoveKind; 021 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.List; 025 026/** 027 * A panel to display some text using libgdx directly (i.e. without using 028 * {@link SquidPanel}) as in these examples (no scrolling first, then with a 029 * scroll bar): 030 * 031 * <p> 032 * <ul> 033 * <li><img src="http://i.imgur.com/EqEXqlu.png"/></li> 034 * <li><img src="http://i.imgur.com/LYbxQZE.png"/></li> 035 * </ul> 036 * </p> 037 * 038 * <p> 039 * It supports vertical scrolling, i.e. it'll put a vertical scrollbar if 040 * there's too much text to display. This class does a lot of stuff, you 041 * typically only have to provide the textures for the scrollbars and the scroll 042 * knobs (see example below). 043 * </p> 044 * 045 * <p> 046 * A typical usage of this class is as follows: 047 * 048 * <pre> 049 * final TextPanel<Color> tp = new TextPanel<>(new GDXMarkup(), font); 050 * tp.init(screenWidth, screenHeight, text); <- first 2 params: for fullscreen 051 * final ScrollPane sp = tp.getScrollPane(); 052 * sp.setScrollPaneStyle(new ScrollPaneStyle(...)); <- set textures 053 * stage.addActor(sp); 054 * stage.setKeyboardFocus(sp); 055 * stage.setScrollFocus(sp); 056 * stage.draw(); 057 * </pre> 058 * </p> 059 * 060 * <p> 061 * In addition to what {@link ScrollPane} does (knobs, handling of the wheel); 062 * this class plugs scrolling with arrow keys (up, down, page up, page down) and 063 * vim shortcuts (j/k). 064 * </p> 065 * 066 * @author smelC 067 * 068 * @see ScrollPane 069 */ 070public class TextPanel<T extends Color> { 071 072 /** 073 * The color to use to paint the background (outside buttons) using 074 * {@link ShapeRenderer}. Or {@code null} to disable background coloring. 075 */ 076 public /* @Nullable */ T backgroundColor; 077 078 /** 079 * The color of the border around this panel, if any. If set, it'll be 080 * rendered using {@link ShapeRenderer} and {@link #borderStyle}. 081 */ 082 public /* @Nullable */ T borderColor; 083 084 /** The size of the border, if any */ 085 public float borderSize; 086 087 public CornerStyle borderStyle = CornerStyle.ROUNDED; 088 089 /** 090 * Whether to use 'j' to scroll down, and 'k' to scroll up. Serious 091 * roguelikes leave that to {@code true}. 092 */ 093 public boolean vimShortcuts = true; 094 095 protected /* @Nullable */ IMarkup<T> markup; 096 097 protected BitmapFont font; 098 protected boolean distanceField; 099 protected TextCellFactory tcf; 100 /** The text to display */ 101 protected List<IColoredString<T>> text; 102 103 protected StringBuilder builder; 104 105 protected final ScrollPane scrollPane; 106 107 /** 108 * The actor whose size is adjusted to the text. When scrolling is required, 109 * it is bigger than {@link #scrollPane}. 110 */ 111 protected final Actor textActor; 112 113 /** Do not access directly, use {@link #getRenderer()} */ 114 private /* @Nullable */ ShapeRenderer renderer; 115 116 /** 117 * The text to display MUST be set later on with 118 * {@link #init(float, float, Collection)}. 119 * 120 * @param markup 121 * An optional way to compute markup. 122 * @param font 123 * The font to use. It can be set later using 124 * {@link #setFont(BitmapFont)}, but it MUST be set before 125 * drawing this panel. 126 */ 127 public TextPanel(/* @Nullable */IMarkup<T> markup, /* @Nullable */ BitmapFont font) { 128 if (markup != null) 129 setMarkup(markup); 130 if (font != null) 131 setFont(font); 132 builder = new StringBuilder(512); 133 textActor = new TextActor(); 134 135 this.scrollPane = new ScrollPane(textActor); 136 137 this.scrollPane.addListener(new InputListener() { 138 @Override 139 public boolean keyDown(InputEvent event, int keycode) { 140 /* To receive key up */ 141 return true; 142 } 143 144 @Override 145 public boolean keyUp(InputEvent event, int keycode) { 146 final YMoveKind d = UIUtil.YMoveKind.of(keycode, vimShortcuts); 147 if (d == null) 148 return false; 149 else { 150 switch (d) { 151 case DOWN: 152 case UP: { 153 handleArrow(!d.isDown()); 154 return true; 155 } 156 case PAGE_DOWN: 157 case PAGE_UP: 158 final float scrollY = scrollPane.getScrollY(); 159 final int mult = d.isDown() ? 1 : -1; 160 scrollPane.setScrollY(scrollY + mult * textActor.getHeight()); 161 return true; 162 } 163 throw new IllegalStateException( 164 "Unmatched " + YMoveKind.class.getSimpleName() + ": " + d); 165 } 166 } 167 168 @Override 169 public boolean keyTyped(InputEvent event, char character) { 170 if (vimShortcuts && (character == 'j' || character == 'k')) 171 return true; 172 else 173 return super.keyTyped(event, character); 174 } 175 176 private void handleArrow(boolean up) { 177 final float scrollY = scrollPane.getScrollY(); 178 final int mult = up ? -1 : 1; 179 scrollPane.setScrollY(scrollY + (scrollPane.getHeight() * 0.8f * mult)); 180 } 181 }); 182 } 183 184 /** 185 * The text to display MUST be set later on with 186 * {@link #init(float, float, Collection)}. 187 * 188 * @param markup 189 * An optional way to compute markup. 190 * @param distanceFieldFont 191 * A distance field font as a TextCellFactory to use. 192 * Won't be used for drawing in cells, just the distance field code it has matters. 193 */ 194 public TextPanel(/* @Nullable */IMarkup<T> markup, /* @Nullable */ TextCellFactory distanceFieldFont) { 195 if (markup != null) 196 setMarkup(markup); 197 if (distanceFieldFont != null) 198 { 199 tcf = distanceFieldFont; 200 distanceField = distanceFieldFont.distanceField; 201 tcf.initBySize(); 202 font = tcf.font(); 203 if (markup != null) 204 font.getData().markupEnabled = true; 205 } 206 builder = new StringBuilder(512); 207 textActor = new TextActor(); 208 scrollPane = new ScrollPane(textActor); 209 210 this.scrollPane.addListener(new InputListener() { 211 @Override 212 public boolean keyDown(InputEvent event, int keycode) { 213 /* To receive key up */ 214 return true; 215 } 216 217 @Override 218 public boolean keyUp(InputEvent event, int keycode) { 219 final YMoveKind d = UIUtil.YMoveKind.of(keycode, vimShortcuts); 220 if (d == null) 221 return false; 222 else { 223 switch (d) { 224 case DOWN: 225 case UP: { 226 handleArrow(!d.isDown()); 227 return true; 228 } 229 case PAGE_DOWN: 230 case PAGE_UP: 231 final float scrollY = scrollPane.getScrollY(); 232 final int mult = d.isDown() ? 1 : -1; 233 scrollPane.setScrollY(scrollY + mult * textActor.getHeight()); 234 return true; 235 } 236 throw new IllegalStateException( 237 "Unmatched " + YMoveKind.class.getSimpleName() + ": " + d); 238 } 239 } 240 241 @Override 242 public boolean keyTyped(InputEvent event, char character) { 243 if (vimShortcuts && (character == 'j' || character == 'k')) 244 return true; 245 else 246 return super.keyTyped(event, character); 247 } 248 249 private void handleArrow(boolean up) { 250 final float scrollY = scrollPane.getScrollY(); 251 final int mult = up ? -1 : 1; 252 scrollPane.setScrollY(scrollY + (scrollPane.getHeight() * 0.8f * mult)); 253 } 254 }); 255 } 256 257 /** 258 * @param m 259 * The markup to use. 260 */ 261 public void setMarkup(IMarkup<T> m) { 262 if (font != null) 263 font.getData().markupEnabled |= true; 264 this.markup = m; 265 } 266 267 /** 268 * Sets the font to use. This method should be called once before 269 * {@link #init(float, float, Collection)} if the font wasn't given at 270 * creation-time. 271 * 272 * @param font 273 * The font to use. 274 */ 275 public void setFont(BitmapFont font) { 276 this.font = font; 277 tcf = new TextCellFactory().font(font).height(MathUtils.ceil(font.getLineHeight())) 278 .width(MathUtils.round(font.getSpaceWidth())); 279 if (markup != null) 280 font.getData().markupEnabled |= true; 281 } 282 283 /** 284 * This method sets the sizes of {@link #scrollPane} and {@link #textActor}. 285 * This method MUST be called before rendering. 286 * 287 * @param maxHeight 288 * The maximum height that the scrollpane can take (equal or 289 * smaller than the height of the text actor). 290 * @param width 291 * The width of the scrollpane and the text actor. 292 * @param text 293 */ 294 public void init(float width, float maxHeight, Collection<? extends IColoredString<T>> text) { 295 this.text = new ArrayList<>(text); 296 297 scrollPane.setWidth(width); 298 textActor.setWidth(width); 299 300 if (tcf == null) 301 throw new NullPointerException( 302 "The font should be set before calling " + TextPanel.class.getSimpleName() + "::init"); 303 304 final BitmapFontCache cache = font.getCache(); 305 final List<String> toDisplay = getTypesetText(); 306 float totalTextHeight = tcf.height(); 307 GlyphLayout layout = cache.addText(builder, 0, 0, 0, builder.length(), width, Align.left, true); 308 totalTextHeight += layout.height; 309 if(totalTextHeight < 0) 310 totalTextHeight = 0; 311 textActor.setHeight(/* Entire height */ totalTextHeight); 312 final boolean yscroll = maxHeight < totalTextHeight; 313 scrollPane.setHeight(/* Maybe not the entire height */ Math.min(totalTextHeight, maxHeight)); 314 scrollPane.setWidget(new TextActor()); 315 yScrollingCallback(yscroll); 316 } 317 318 public void init(float width, float maxHeight, T color, String... text) 319 { 320 ArrayList<IColoredString.Impl<T>> coll = new ArrayList<>(text.length); 321 for(String t : text) 322 { 323 coll.add(new IColoredString.Impl<T>(t, color)); 324 } 325 init(width, maxHeight, coll); 326 } 327 328 /** 329 * Draws the border. You have to call this method manually, because the 330 * border is outside the actor and hence should be drawn at the very end, 331 * otherwise it can get overwritten by UI element. 332 * 333 * @param batch 334 */ 335 public void drawBorder(Batch batch) { 336 if (borderColor != null && 0 < borderSize) { 337 final boolean reset = batch.isDrawing(); 338 if (reset) 339 batch.end(); 340 341 final ShapeRenderer sr = getRenderer(); 342 final Matrix4 m = batch.getTransformMatrix(); 343 sr.setTransformMatrix(m); 344 sr.begin(ShapeType.Filled); 345 sr.setColor(borderColor); 346 UIUtil.drawMarginsAround(sr, scrollPane.getX(), scrollPane.getY(), scrollPane.getWidth(), 347 scrollPane.getHeight() - 1, borderSize, borderColor, borderStyle, 1f, 1f); 348 sr.end(); 349 350 if (reset) 351 batch.begin(); 352 } 353 } 354 355 /** 356 * @return The text to draw, after applying {@link #present(IColoredString)} 357 * and {@link #applyMarkup(IColoredString)}. 358 */ 359 public /* @Nullable */ List<String> getTypesetText() { 360 if (text == null) 361 return null; 362 builder.delete(0, builder.length()); 363 final List<String> result = new ArrayList<>(); 364 for (IColoredString<T> line : text) { 365 /* This code must be consistent with #draw in the custom Actor */ 366 final IColoredString<T> tmp = present(line); 367 final String marked = applyMarkup(tmp); 368 result.add(marked); 369 builder.append(marked); 370 builder.append('\n'); 371 } 372 if(builder.length() > 0) 373 builder.deleteCharAt(builder.length() - 1); 374 return result; 375 } 376 377 /** 378 * @return The {@link ScrollPane} containing {@link #getTextActor()}. 379 */ 380 public ScrollPane getScrollPane() { 381 return scrollPane; 382 } 383 384 /** 385 * @return The {@link Actor} where the text is drawn. It may be bigger than 386 * {@link #getScrollPane()}. 387 */ 388 public Actor getTextActor() { 389 return textActor; 390 } 391 392 /** 393 * @return The font used, if set. 394 */ 395 public /* @Nullable */ BitmapFont getFont() { 396 return font; 397 } 398 399 public void dispose() { 400 if (renderer != null) 401 renderer.dispose(); 402 } 403 404 /** 405 * Callback done to do stuff according to whether y-scrolling is required 406 * 407 * @param required 408 * Whether y scrolling is required. 409 */ 410 protected void yScrollingCallback(boolean required) { 411 if (required) { 412 /* Disable borders, they don't mix well with scrollbars */ 413 borderSize = 0; 414 scrollPane.setFadeScrollBars(false); 415 scrollPane.setForceScroll(false, true); 416 } 417 } 418 419 /** 420 * @param ics 421 * Text set when building {@code this} 422 * @return The text to display to screen. If you wanna 423 * {@link squidpony.IColorCenter#filter(IColoredString) filter} your 424 * text , do it here. 425 */ 426 protected IColoredString<T> present(IColoredString<T> ics) { 427 return ics; 428 } 429 430 /** 431 * @param ics 432 * @return The text obtained after applying {@link #markup}. 433 */ 434 protected String applyMarkup(IColoredString<T> ics) { 435 return markup == null ? ics.toString() : ics.presentWithMarkup(markup); 436 } 437 438 /** 439 * @return A fresh renderer. 440 */ 441 protected ShapeRenderer buildRenderer() { 442 return new ShapeRenderer(); 443 } 444 445 /** 446 * @return The renderer to use. 447 */ 448 protected ShapeRenderer getRenderer() { 449 if (renderer == null) 450 renderer = buildRenderer(); 451 return renderer; 452 } 453 454 private class TextActor extends Actor 455 { 456 TextActor() 457 { 458 459 } 460 @Override 461 public void draw(Batch batch, float parentAlpha) { 462 463 final float tx = 0;//getX(); 464 final float ty = 0;//getY(); 465 final float twidth = getWidth(); 466 final float theight = getHeight(); 467 468 final float height = scrollPane.getHeight(); 469 470 if (backgroundColor != null) { 471 batch.setColor(backgroundColor); 472 batch.draw(tcf.getSolid(), tx, ty, twidth, theight); 473 batch.setColor(Color.WHITE); 474 /* 475 batch.end(); 476 477 final Matrix4 m = batch.getTransformMatrix(); 478 final ShapeRenderer sr = getRenderer(); 479 sr.setTransformMatrix(m); 480 sr.begin(ShapeType.Filled); 481 sr.setColor(backgroundColor); 482 UIUtil.drawRectangle(renderer, tx, ty, twidth, theight, ShapeType.Filled, 483 backgroundColor); 484 sr.end(); 485 486 batch.begin(); 487 */ 488 } 489 490 if (font == null) 491 throw new NullPointerException( 492 "The font should be set when drawing a " + getClass().getSimpleName()); 493 if (text == null) 494 throw new NullPointerException( 495 "The text should be set when drawing a " + getClass().getSimpleName()); 496 if (tcf != null) { 497 tcf.configureShader(batch); 498 } 499 float yscroll = scrollPane.getScrollY(); 500 501 final float destx = tx, offY = (tcf != null) ? tcf.height * 0.5f : 0; 502 getTypesetText(); 503 font.draw(batch, builder, destx, theight + yscroll - offY, 504 0, builder.length(), twidth, Align.left, true); 505 506 } 507 } 508 509}