001package squidpony.panel; 002 003import squidpony.annotation.Beta; 004 005import java.util.*; 006 007/** 008 * A {@link String} divided in chunks of different colors. Use the 009 * {@link Iterable} interface to get the pieces. 010 * 011 * @author smelC 012 * 013 * @param <T> 014 * The type of colors; 015 */ 016@Beta 017public interface IColoredString<T> extends Iterable<IColoredString.Bucket<T>> { 018 019 /** 020 * Mutates {@code this} by appending {@code c} to it. 021 * 022 * @param c 023 * The text to append. 024 * @param color 025 * {@code text}'s color. Or {@code null} to let the panel decide. 026 */ 027 void append(char c, /* @Nullable */T color); 028 029 /** 030 * Mutates {@code this} by appending {@code text} to it. Does nothing if 031 * {@code text} is {@code null}. 032 * 033 * @param text 034 * The text to append. 035 * @param color 036 * {@code text}'s color. Or {@code null} to let the panel decide. 037 */ 038 void append(/* @Nullable */String text, /* @Nullable */T color); 039 040 /** 041 * Mutates {@code this} by appending {@code i} to it. 042 * 043 * @param i 044 * The int to append. 045 * @param color 046 * {@code text}'s color. Or {@code null} to let the panel decide. 047 */ 048 void appendInt(int i, /* @Nullable */T color); 049 050 /** 051 * Mutates {@code this} by appending {@code f} to it. 052 * 053 * @param f 054 * The float to append. 055 * @param color 056 * {@code text}'s color. Or {@code null} to let the panel decide. 057 */ 058 void appendFloat(float f, /* @Nullable */T color); 059 060 /** 061 * Mutates {@code this} by appending {@code other} to it. 062 * 063 * @param other 064 */ 065 void append(IColoredString<T> other); 066 067 /** 068 * Deletes all content after index {@code len} (if any). 069 * 070 * @param len 071 */ 072 void setLength(int len); 073 074 /** 075 * @return {@code true} if {@link #present()} is {@code ""}. 076 */ 077 boolean isEmpty(); 078 079 /** 080 * @param width 081 * A positive integer 082 * @return {@code this} split in pieces that would fit in a display with 083 * {@code width} columns (if all words in {@code this} are smaller 084 * or equal in length to {@code width}, otherwise wrapping will fail 085 * for these words). 086 */ 087 List<IColoredString<T>> wrap(int width); 088 089 /** 090 * This method does NOT guarantee that the result's length is {@code width}. 091 * It is impossible to do correct justifying if {@code this}'s length is 092 * greater than {@code width} or if {@code this} has no space character. 093 * 094 * @param width 095 * @return A variant of {@code this} where spaces have been introduced 096 * in-between words, so that {@code this}'s length is as close as 097 * possible to {@code width}. Or {@code this} itself if unaffected. 098 */ 099 IColoredString<T> justify(int width); 100 101 /** 102 * Empties {@code this}. 103 */ 104 void clear(); 105 106 /** 107 * This method is typically more efficient than {@link #colorAt(int)}. 108 * 109 * @return The color of the last bucket, if any. 110 */ 111 /* @Nullable */ T lastColor(); 112 113 /** 114 * @param index 115 * @return The color at {@code index}, if any. 116 * @throws NoSuchElementException 117 * If {@code index} equals or is greater to {@link #length()}. 118 */ 119 /* @Nullable */ T colorAt(int index); 120 121 /** 122 * @return The length of text. 123 */ 124 int length(); 125 126 /** 127 * @return The text that {@code this} represents. 128 */ 129 String present(); 130 131 /** 132 * Given some way of converting from a T value to an in-line markup tag, returns a string representation of 133 * this IColoredString with in-line markup representing colors. 134 * @param markup an IMarkup implementation 135 * @return a String with markup inserted inside. 136 */ 137 String presentWithMarkup(IMarkup<T> markup); 138 139 /** 140 * A basic implementation of {@link IColoredString}. 141 * 142 * @author smelC 143 * 144 * @param <T> 145 * The type of colors 146 */ 147 class Impl<T> implements IColoredString<T> { 148 149 protected final LinkedList<Bucket<T>> fragments; 150 151 /** 152 * An empty instance. 153 */ 154 public Impl() { 155 fragments = new LinkedList<>(); 156 } 157 158 /** 159 * An instance initially containing {@code text} (with {@code color}). 160 * 161 * @param text 162 * The text that {@code this} should contain. 163 * @param color 164 * The color of {@code text}. 165 */ 166 public Impl(String text, /* @Nullable */T color) { 167 this(); 168 169 append(text, color); 170 } 171 172 /** 173 * A static constructor, to avoid having to write {@code <T>} in the 174 * caller. 175 * 176 * @return {@code new Impl(s, t)}. 177 */ 178 public static <T> IColoredString.Impl<T> create() { 179 return new IColoredString.Impl<>("", null); 180 } 181 182 /** 183 * A convenience method, equivalent to {@code create(s, null)}. 184 * 185 * @param s 186 * @return {@code create(s, null)} 187 */ 188 public static <T> IColoredString.Impl<T> create(String s) { 189 return create(s, null); 190 } 191 192 /** 193 * A static constructor, to avoid having to write {@code <T>} in the 194 * caller. 195 * 196 * @return {@code new Impl(s, t)}. 197 */ 198 public static <T> IColoredString.Impl<T> create(String s, /* @Nullable */ T t) { 199 return new IColoredString.Impl<>(s, t); 200 } 201 202 @Override 203 public void append(char c, T color) { 204 append(String.valueOf(c), color); 205 } 206 207 @Override 208 public void append(String text, T color) { 209 if (text == null || text.isEmpty()) 210 return; 211 212 if (fragments.isEmpty()) 213 fragments.add(new Bucket<>(text, color)); 214 else { 215 final Bucket<T> last = fragments.getLast(); 216 if (equals(last.color, color)) { 217 /* Append to the last bucket, to avoid extending the list */ 218 final Bucket<T> novel = last.append(text); 219 fragments.removeLast(); 220 fragments.addLast(novel); 221 } else 222 fragments.add(new Bucket<>(text, color)); 223 } 224 } 225 226 @Override 227 public void appendInt(int i, T color) { 228 append(String.valueOf(i), color); 229 } 230 231 @Override 232 public void appendFloat(float f, T color) { 233 final int i = Math.round(f); 234 append(i == f ? String.valueOf(i) : String.valueOf(f), color); 235 } 236 237 @Override 238 /* KISS implementation */ 239 public void append(IColoredString<T> other) { 240 for (IColoredString.Bucket<T> ofragment : other) 241 append(ofragment.getText(), ofragment.getColor()); 242 } 243 244 public void append(Bucket<T> bucket) { 245 this.fragments.add(new Bucket<>(bucket.getText(), bucket.getColor())); 246 } 247 248 @Override 249 public void setLength(int len) { 250 int l = 0; 251 final ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator(); 252 while (it.hasNext()) { 253 final IColoredString.Bucket<T> next = it.next(); 254 final String ftext = next.text; 255 final int flen = ftext.length(); 256 final int nextl = l + flen; 257 if (nextl < len) { 258 /* Nothing to do */ 259 l += flen; 260 continue; 261 } else if (nextl == len) { 262 /* Delete all next fragments */ 263 while (it.hasNext()) { 264 it.next(); 265 it.remove(); 266 } 267 /* We'll exit the outer loop right away */ 268 } else { 269 assert len < nextl; 270 /* Trim this fragment */ 271 final IColoredString.Bucket<T> trimmed = next.setLength(len - l); 272 /* Replace this fragment */ 273 it.remove(); 274 it.add(trimmed); 275 /* Delete all next fragments */ 276 while (it.hasNext()) { 277 it.next(); 278 it.remove(); 279 } 280 /* We'll exit the outer loop right away */ 281 } 282 } 283 } 284 285 @Override 286 public List<IColoredString<T>> wrap(int width) { 287 if (width <= 0) { 288 /* Really, you should not rely on this behavior */ 289 System.err.println("Cannot wrap string in empty display"); 290 final List<IColoredString<T>> result = new LinkedList<>(); 291 result.add(this); 292 return result; 293 } 294 295 final List<IColoredString<T>> result = new ArrayList<>(); 296 if (isEmpty()) { 297 /* 298 * Catch this case early on, as empty lines are eaten below (see 299 * code after the while). Checking emptyness is cheap anyway. 300 */ 301 result.add(this); 302 return result; 303 } 304 305 IColoredString<T> current = create(); 306 int curlen = 0; 307 final Iterator<Bucket<T>> it = iterator(); 308 while (it.hasNext()) { 309 final Bucket<T> next = it.next(); 310 final String bucket = next.getText(); 311 final String[] split = bucket.split(" "); 312 final T color = next.color; 313 for (int i = 0; i < split.length; i++) { 314 if (i == split.length - 1 && bucket.endsWith(" ")) 315 /* 316 * Do not loose trailing space that got eaten by 317 * 'bucket.split'. 318 */ 319 split[i] = split[i] + " "; 320 final String chunk = split[i]; 321 final int chunklen = chunk.length(); 322 final boolean addLeadingSpace = 0 < curlen && 0 < i; 323 if (curlen + chunklen + (addLeadingSpace ? 1 : 0) <= width) { 324 if (addLeadingSpace) { 325 /* 326 * Do not forget space on which chunk got split. If 327 * the space is offscreen, it's harmless, hence not 328 * checking it. 329 */ 330 current.append(' ', null); 331 curlen++; 332 } 333 334 /* Can add it */ 335 current.append(chunk, color); 336 /* Extend size */ 337 curlen += chunklen; 338 } else { 339 /* Need to wrap */ 340 /* Flush content so far */ 341 if (!current.isEmpty()) 342 result.add(current); 343 /* 344 * else: line was prepared, but did not contain anything 345 */ 346 if (chunklen <= width) { 347 curlen = chunklen; 348 current = create(); 349 current.append(chunk, color); 350 /* Reinit size */ 351 curlen = chunklen; 352 } else { 353 /* 354 * This word is too long. Adding it and preparing a 355 * new line immediately. 356 */ 357 /* Add */ 358 result.add(new Impl<>(chunk, color)); 359 /* Prepare for next rolls */ 360 current = create(); 361 /* Reinit size */ 362 curlen = 0; 363 } 364 } 365 } 366 } 367 368 if (!current.isEmpty()) { 369 /* Flush rest */ 370 result.add(current); 371 } 372 373 return result; 374 } 375 376 @Override 377 /* 378 * smelC: not the cutest result (we should add spaces both from the left 379 * and the right, instead of just from the left), but better than 380 * nothing. 381 */ 382 public IColoredString<T> justify(int width) { 383 int length = length(); 384 385 if (width <= length) 386 /* 387 * If width==length, we're good. If width<length, we cannot 388 * adjust 389 */ 390 return this; 391 392 int totalDiff = width - length; 393 assert 0 < totalDiff; 394 395 if (width <= totalDiff * 3) 396 /* Too much of a difference, it would look very weird. */ 397 return this; 398 399 final IColoredString.Impl<T> result = create(); 400 401 ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator(); 402 final int nbb = fragments.size(); 403 final int[] bucketToNbSpaces = new int[nbb]; 404 /* The number of buckets that can contribute to justifying */ 405 int totalNbSpaces = 0; 406 /* The index of the last bucket that has spaces */ 407 int lastHopeIndex = -1; 408 { 409 int i = 0; 410 while (it.hasNext()) { 411 final Bucket<T> next = it.next(); 412 final int nbs = nbSpaces(next.getText()); 413 totalNbSpaces += nbs; 414 bucketToNbSpaces[i] = nbs; 415 i++; 416 } 417 418 if (totalNbSpaces == 0) 419 /* Cannot do anything */ 420 return this; 421 422 for (int j = bucketToNbSpaces.length - 1; 0 <= j; j--) { 423 if (0 < bucketToNbSpaces[j]) { 424 lastHopeIndex = j; 425 break; 426 } 427 } 428 /* Holds because we ruled out 'totalNbSpaces == 0' before */ 429 assert 0 <= lastHopeIndex; 430 } 431 432 int toAddPerSpace = totalNbSpaces == 0 ? 0 : (totalDiff / totalNbSpaces); 433 int totalRest = totalDiff - (toAddPerSpace * totalNbSpaces); 434 assert 0 <= totalRest; 435 436 int bidx = -1; 437 438 it = fragments.listIterator(); 439 440 while (it.hasNext() && 0 < totalDiff) { 441 bidx++; 442 final Bucket<T> next = it.next(); 443 final String bucket = next.getText(); 444 final int blength = bucket.length(); 445 final int localNbSpaces = bucketToNbSpaces[bidx]; 446 if (localNbSpaces == 0) { 447 /* Cannot change it */ 448 result.append(next); 449 continue; 450 } 451 int localDiff = localNbSpaces * toAddPerSpace; 452 assert localDiff <= totalDiff; 453 int nb = localDiff / localNbSpaces; 454 int localRest = localDiff - (nb * localNbSpaces); 455 if (localRest == 0 && 0 < totalRest) { 456 /* 457 * Take one for the group. This avoids flushing all spaces 458 * needed in the 'last hope' cases below. 459 */ 460 localRest = 1; 461 } 462 assert 0 <= localRest; 463 assert localRest <= totalRest; 464 String novel = ""; 465 int eatenSpaces = 1; 466 for (int i = 0; i < blength; i++) { 467 final char c = bucket.charAt(i); 468 novel += c; 469 if (c == ' ' && (0 < localDiff || 0 < totalDiff || 0 < localRest || 0 < totalRest)) { 470 /* Can (and should) add an extra space */ 471 for (int j = 0; j < nb && 0 < localDiff; j++) { 472 novel += " "; 473 localDiff--; 474 totalDiff--; 475 } 476 if (0 < localRest || 0 < totalRest) { 477 if (eatenSpaces == localNbSpaces) { 478 /* I'm the last hope for this bucket */ 479 for (int j = 0; j < localRest; j++) { 480 novel += " "; 481 localRest--; 482 totalRest--; 483 } 484 if (bidx == lastHopeIndex) { 485 /* I'm the last hope globally */ 486 while (0 < totalRest) { 487 novel += " "; 488 totalRest--; 489 } 490 } 491 } else { 492 if (0 < localRest && 0 < totalRest) { 493 /* Not the last hope: take one only */ 494 novel += " "; 495 localRest--; 496 totalRest--; 497 } 498 } 499 } 500 eatenSpaces++; 501 } 502 } 503 /* I did my job */ 504 assert localRest == 0; 505 /* If I was the hope, I did my job */ 506 assert bidx != lastHopeIndex || totalRest == 0; 507 result.append(novel, next.getColor()); 508 } 509 510 while (it.hasNext()) 511 result.append(it.next()); 512 513 return result; 514 } 515 516 @Override 517 public void clear() { 518 fragments.clear(); 519 } 520 521 @Override 522 public int length() { 523 int result = 0; 524 for (Bucket<T> fragment : fragments) 525 result += fragment.getText().length(); 526 return result; 527 } 528 529 @Override 530 /* This implementation is resilient to empty buckets */ 531 public boolean isEmpty() { 532 for (Bucket<?> bucket : fragments) { 533 if (bucket.text == null || bucket.text.isEmpty()) 534 continue; 535 else 536 return false; 537 } 538 return true; 539 } 540 541 @Override 542 public T lastColor() { 543 return fragments.isEmpty() ? null : fragments.getLast().color; 544 } 545 546 @Override 547 public T colorAt(int index) { 548 final ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator(); 549 int now = 0; 550 while (it.hasNext()) { 551 final IColoredString.Bucket<T> next = it.next(); 552 final String ftext = next.text; 553 final int flen = ftext.length(); 554 final int nextl = now + flen; 555 if (index <= nextl) 556 return next.color; 557 now += flen; 558 } 559 throw new NoSuchElementException("Character at index " + index + " in " + this); 560 } 561 562 @Override 563 public String present() { 564 final StringBuilder result = new StringBuilder(); 565 for (Bucket<T> fragment : fragments) 566 result.append(fragment.text); 567 return result.toString(); 568 } 569 570 /** 571 * Given some way of converting from a T value to an in-line markup tag, returns a string representation of 572 * this IColoredString with in-line markup representing colors. 573 * @param markup an IMarkup implementation 574 * @return a String with markup inserted inside. 575 */ 576 @Override 577 public String presentWithMarkup(IMarkup<T> markup) { 578 final StringBuilder result = new StringBuilder(); 579 for (Bucket<T> fragment : fragments) { 580 if(fragment.color != null) 581 result.append(markup.getMarkup(fragment.color)); 582 else 583 result.append(markup.closeMarkup()); 584 result.append(fragment.text); 585 } 586 result.append(markup.closeMarkup()); 587 return result.toString(); 588 } 589 590 @Override 591 public Iterator<Bucket<T>> iterator() { 592 return fragments.iterator(); 593 } 594 595 @Override 596 public String toString() { 597 return present(); 598 } 599 600 protected static boolean equals(Object o1, Object o2) { 601 if (o1 == null) 602 return o2 == null; 603 else 604 return o1.equals(o2); 605 } 606 607 private int nbSpaces(String s) { 608 final int bd = s.length(); 609 int result = 0; 610 for (int i = 0; i < bd; i++) { 611 final char c = s.charAt(i); 612 if (c == ' ') 613 result++; 614 } 615 return result; 616 } 617 } 618 619 /** 620 * A piece of a {@link IColoredString}: a text and its color. 621 * 622 * @author smelC 623 * 624 * @param <T> 625 * The type of colors; 626 */ 627 class Bucket<T> { 628 629 protected final String text; 630 protected final/* @Nullable */T color; 631 632 public Bucket(String text, /* @Nullable */T color) { 633 this.text = text == null ? "" : text; 634 this.color = color; 635 } 636 637 /** 638 * @param text 639 * @return An instance whose text is {@code this.text + text}. Color is 640 * unchanged. 641 */ 642 public Bucket<T> append(String text) { 643 if (text == null || text.isEmpty()) 644 /* Let's save an allocation */ 645 return this; 646 else 647 return new Bucket<>(this.text + text, color); 648 } 649 650 public Bucket<T> setLength(int l) { 651 final int here = text.length(); 652 if (here <= l) 653 return this; 654 else 655 return new Bucket<>(text.substring(0, l), color); 656 } 657 658 /** 659 * @return The text that this bucket contains. 660 */ 661 public String getText() { 662 return text; 663 } 664 665 /** 666 * @return The color of {@link #getText()}. Or {@code null} if none. 667 */ 668 public/* @Nullable */T getColor() { 669 return color; 670 } 671 672 @Override 673 public String toString() { 674 if (color == null) 675 return text; 676 else 677 return text + "(" + color + ")"; 678 } 679 680 } 681}