001package squidpony.panel; 002 003import squidpony.StringKit; 004 005import java.util.ArrayList; 006import java.util.Iterator; 007import java.util.List; 008import java.util.ListIterator; 009import java.util.NoSuchElementException; 010 011/** 012 * A {@link String} divided in chunks of different colors. Use the 013 * {@link Iterable} interface to get the pieces. 014 * 015 * @author smelC 016 * 017 * @param <T> 018 * The type of colors; 019 */ 020public interface IColoredString<T> extends Iterable<IColoredString.Bucket<T>> { 021 022 /** 023 * A convenience alias for {@code append(c, null)}. 024 * 025 * @param c the char to append 026 */ 027 void append(/* @Nullable */ char c); 028 029 /** 030 * Mutates {@code this} by appending {@code c} to it. 031 * 032 * @param c 033 * The text to append. 034 * @param color 035 * {@code text}'s color. Or {@code null} to let the panel decide. 036 */ 037 void append(char c, /* @Nullable */T color); 038 039 /** 040 * A convenience alias for {@code append(text, null)}. 041 * 042 * @param text 043 */ 044 void append(/* @Nullable */ String text); 045 046 /** 047 * Mutates {@code this} by appending {@code text} to it. Does nothing if 048 * {@code text} is {@code null}. 049 * 050 * @param text 051 * The text to append. 052 * @param color 053 * {@code text}'s color. Or {@code null} to let the panel decide. 054 */ 055 void append(/* @Nullable */String text, /* @Nullable */T color); 056 057 /** 058 * Mutates {@code this} by appending {@code i} to it. 059 * 060 * @param i 061 * The int to append. 062 * @param color 063 * {@code text}'s color. Or {@code null} to let the panel decide. 064 */ 065 void appendInt(int i, /* @Nullable */T color); 066 067 /** 068 * Mutates {@code this} by appending {@code f} to it. 069 * 070 * @param f 071 * The float to append. 072 * @param color 073 * {@code text}'s color. Or {@code null} to let the panel decide. 074 */ 075 void appendFloat(float f, /* @Nullable */T color); 076 077 /** 078 * Mutates {@code this} by appending {@code other} to it. 079 * 080 * @param other 081 */ 082 void append(IColoredString<T> other); 083 084 /** 085 * Replace color {@code old} by {@code new_} in all buckets of {@code this}. 086 * 087 * @param old 088 * The color to replace. 089 * @param new_ 090 * The replacing color. 091 */ 092 void replaceColor(/* @Nullable */ T old, /* @Nullable */ T new_); 093 094 /** 095 * Set {@code color} in all buckets. 096 * 097 * @param color 098 */ 099 void setColor(/*@Nullable*/ T color); 100 101 /** 102 * Deletes all content after index {@code len} (if any). 103 * 104 * @param len 105 */ 106 void setLength(int len); 107 108 /** 109 * @return {@code true} if {@link #present()} is {@code ""}. 110 */ 111 boolean isEmpty(); 112 113 /** 114 * @param width 115 * A positive integer 116 * @return {@code this} split in pieces that would fit in a display with 117 * {@code width} columns (if all words in {@code this} are smaller 118 * or equal in length to {@code width}, otherwise wrapping will fail 119 * for these words). 120 */ 121 List<IColoredString<T>> wrap(int width); 122 123 /** 124 * @param width 125 * A positive integer 126 * @param buf 127 * A List of IColoredString with the same T type as this; will have the wrapped contents appended to it. 128 * Cannot be null, and if it has existing contents, they will be left as-is. 129 * @return {@code buf} containing {@code this} split in pieces that would fit in a display with 130 * {@code width} columns (if all words in {@code this} are smaller 131 * or equal in length to {@code width}, otherwise wrapping will fail 132 * for these words). 133 */ 134 135 List<IColoredString<T>> wrap(int width, List<IColoredString<T>> buf); 136 137 /** 138 * This method does NOT guarantee that the result's length is {@code width}. 139 * It is impossible to do correct justifying if {@code this}'s length is 140 * greater than {@code width} or if {@code this} has no space character. 141 * 142 * @param width 143 * @return A variant of {@code this} where spaces have been introduced 144 * in-between words, so that {@code this}'s length is as close as 145 * possible to {@code width}. Or {@code this} itself if unaffected. 146 */ 147 IColoredString<T> justify(int width); 148 149 /** 150 * Empties {@code this}. 151 */ 152 void clear(); 153 154 /** 155 * This method is typically more efficient than {@link #colorAt(int)}. 156 * 157 * @return The color of the last bucket, if any. 158 */ 159 /* @Nullable */ T lastColor(); 160 161 /** 162 * @param index 163 * @return The color at {@code index}, if any. 164 * @throws NoSuchElementException 165 * If {@code index} equals or is greater to {@link #length()}. 166 */ 167 /* @Nullable */ T colorAt(int index); 168 169 /** 170 * @param index 171 * @return The character at {@code index}, if any. 172 * @throws NoSuchElementException 173 * If {@code index} equals or is greater to {@link #length()}. 174 */ 175 char charAt(int index); 176 177 /** 178 * @return The length of text. 179 */ 180 int length(); 181 182 /** 183 * @return The text that {@code this} represents. 184 */ 185 String present(); 186 187 /** 188 * Given some way of converting from a T value to an in-line markup tag, returns a string representation of 189 * this IColoredString with in-line markup representing colors. 190 * @param markup an IMarkup implementation 191 * @return a String with markup inserted inside. 192 */ 193 String presentWithMarkup(IMarkup<T> markup); 194 195 /** 196 * Gets the Buckets as an ArrayList, allowing access by index instead of by {@link #iterator()}. 197 * @return the Buckets that would be returned by {@link #iterator()}, but in an ArrayList. 198 */ 199 ArrayList<Bucket<T>> getFragments(); 200 201 /** 202 * A basic implementation of {@link IColoredString}. 203 * 204 * @author smelC 205 * 206 * @param <T> 207 * The type of colors 208 */ 209 class Impl<T> implements IColoredString<T> { 210 211 protected final ArrayList<Bucket<T>> fragments; 212 213 /** 214 * An empty instance. 215 */ 216 public Impl() { 217 fragments = new ArrayList<>(); 218 } 219 220 /** 221 * An instance initially containing {@code text} (with {@code color}). 222 * 223 * @param text 224 * The text that {@code this} should contain. 225 * @param color 226 * The color of {@code text}. 227 */ 228 public Impl(String text, /* @Nullable */T color) { 229 this(); 230 231 append(text, color); 232 } 233 234 /** 235 * A static constructor, to avoid having to write {@code <T>} in the 236 * caller. 237 * 238 * @return {@code new Impl(s, t)}. 239 */ 240 public static <T> IColoredString.Impl<T> create() { 241 return new IColoredString.Impl<>("", null); 242 } 243 244 /** 245 * A convenience method, equivalent to {@code create(s, null)}. 246 * 247 * @param s 248 * @return {@code create(s, null)} 249 */ 250 public static <T> IColoredString.Impl<T> create(String s) { 251 return create(s, null); 252 } 253 254 /** 255 * A static constructor, to avoid having to write {@code <T>} in the 256 * caller. 257 * 258 * @return {@code new Impl(s, t)}. 259 */ 260 public static <T> IColoredString.Impl<T> create(String s, /* @Nullable */ T t) { 261 return new IColoredString.Impl<>(s, t); 262 } 263 264 public static <T> IColoredString.Impl<T> clone(IColoredString<T> toClone) { 265 final IColoredString.Impl<T> result = new IColoredString.Impl<T>(); 266 result.append(toClone); 267 return result; 268 } 269 270 /** 271 * @param one 272 * @param two 273 * @return Whether {@code one} represents the same content as 274 * {@code two}. 275 */ 276 /* 277 * Method could be smarter, i.e. return true more often, by doing some 278 * normalization. It is unnecessary if you only create instances of 279 * IColoredString.Impl. 280 */ 281 public static <T> boolean equals(IColoredString<T> one, IColoredString<T> two) { 282 if (one == two) 283 return true; 284 285 final Iterator<IColoredString.Bucket<T>> oneIt = one.iterator(); 286 final Iterator<IColoredString.Bucket<T>> twoIt = two.iterator(); 287 while (true) { 288 if (oneIt.hasNext()) { 289 if (twoIt.hasNext()) { 290 final Bucket<T> oneb = oneIt.next(); 291 final Bucket<T> twob = twoIt.next(); 292 if (!equals(oneb.getText(), twob.getText())) 293 return false; 294 if (!equals(oneb.getColor(), twob.getColor())) 295 return false; 296 } else 297 /* 'this' not terminated, but 'other' is. */ 298 return false; 299 } else { 300 if (twoIt.hasNext()) 301 /* 'this' terminated, but not 'other'. */ 302 return false; 303 else 304 /* Both terminated */ 305 break; 306 } 307 308 } 309 return true; 310 } 311 312 @Override 313 public void append(char c) { 314 append(c, null); 315 } 316 317 @Override 318 public void append(char c, T color) { 319 append(String.valueOf(c), color); 320 } 321 322 @Override 323 public void append(String text) { 324 append(text, null); 325 } 326 327 @Override 328 public void append(String text, T color) { 329 if (text == null || text.isEmpty()) 330 return; 331 332 if (fragments.isEmpty()) 333 fragments.add(new Bucket<>(text, color)); 334 else { 335 final Bucket<T> last = fragments.get(fragments.size() - 1); 336 if (equals(last.color, color)) { 337 /* Append to the last bucket, to avoid extending the list */ 338 final Bucket<T> novel = last.append(text); 339 fragments.remove(fragments.size() - 1); 340 fragments.add(novel); 341 } else 342 fragments.add(new Bucket<>(text, color)); 343 } 344 } 345 346 @Override 347 public void appendInt(int i, T color) { 348 append(String.valueOf(i), color); 349 } 350 351 @Override 352 public void appendFloat(float f, T color) { 353 final int i = Math.round(f); 354 append(i == f ? String.valueOf(i) : String.valueOf(f), color); 355 } 356 357 @Override 358 /* KISS implementation */ 359 public void append(IColoredString<T> other) { 360 for (IColoredString.Bucket<T> ofragment : other) 361 append(ofragment.getText(), ofragment.getColor()); 362 } 363 364 @Override 365 public void replaceColor(/* @Nullable */ T old, /* @Nullable */ T new_) { 366 if (equals(old, new_)) 367 /* Nothing to do */ 368 return; 369 370 final ListIterator<Bucket<T>> it = fragments.listIterator(); 371 while (it.hasNext()) { 372 final Bucket<T> bucket = it.next(); 373 if (equals(bucket.color, old)) { 374 /* Replace */ 375 it.remove(); 376 it.add(new Bucket<T>(bucket.getText(), new_)); 377 } 378 /* else leave untouched */ 379 } 380 } 381 382 @Override 383 public void setColor(T color) { 384 final ListIterator<Bucket<T>> it = fragments.listIterator(); 385 while (it.hasNext()) { 386 final Bucket<T> next = it.next(); 387 if (!equals(color, next.getColor())) { 388 it.remove(); 389 it.add(next.setColor(color)); 390 } 391 } 392 } 393 394 public void append(Bucket<T> bucket) { 395 this.fragments.add(new Bucket<>(bucket.getText(), bucket.getColor())); 396 } 397 398 @Override 399 public void setLength(int len) { 400 int l = 0; 401 final ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator(); 402 while (it.hasNext()) { 403 final IColoredString.Bucket<T> next = it.next(); 404 final String ftext = next.text; 405 final int flen = ftext.length(); 406 final int nextl = l + flen; 407 if (nextl < len) { 408 /* Nothing to do */ 409 l += flen; 410 } else if (nextl == len) { 411 /* Delete all next fragments */ 412 while (it.hasNext()) { 413 it.next(); 414 it.remove(); 415 } 416 /* We'll exit the outer loop right away */ 417 } else { 418 /* Trim this fragment */ 419 final IColoredString.Bucket<T> trimmed = next.setLength(len - l); 420 /* Replace this fragment */ 421 it.remove(); 422 it.add(trimmed); 423 /* Delete all next fragments */ 424 while (it.hasNext()) { 425 it.next(); 426 it.remove(); 427 } 428 /* We'll exit the outer loop right away */ 429 } 430 } 431 } 432 433 @Override 434 public List<IColoredString<T>> wrap(int width) 435 { 436 // the one special case that wouldn't involve buf 437 if (width <= 0) { 438 /* Really, you should not rely on this behavior */ 439 System.err.println("Cannot wrap string in empty display"); 440 final List<IColoredString<T>> result = new ArrayList<>(); 441 result.add(this); 442 return result; 443 } 444 // delegate to the buf-appending overload 445 return wrap(width, new ArrayList<IColoredString<T>>()); 446 } 447 448 @Override 449 public List<IColoredString<T>> wrap(int width, List<IColoredString<T>> buf) { 450 if (width <= 0) { 451 /* Really, you should not rely on this behavior */ 452 System.err.println("Cannot wrap string in empty display"); 453 final List<IColoredString<T>> result = new ArrayList<>(); 454 result.add(this); 455 return result; 456 } 457 if (isEmpty()) { 458 /* 459 * Catch this case early on, as empty lines are eaten below (see 460 * code after the while). Checking emptiness is cheap anyway. 461 */ 462 buf.add(this); 463 return buf; 464 } 465 466 IColoredString<T> current = create(); 467 int curlen = 0; 468 final Iterator<Bucket<T>> it = iterator(); 469 while (it.hasNext()) { 470 final Bucket<T> next = it.next(); 471 final String bucket = next.getText(); 472 final String[] split = StringKit.split(bucket," "); 473 final T color = next.color; 474 for (int i = 0; i < split.length; i++) { 475 // This section was needed when using String.split() above, but not for 476 // StringKit.split(), which keeps leading and trailing delimiters. 477// if (i == split.length - 1 && bucket.endsWith(" ")) 478// /* 479// * Do not lose trailing space that got eaten by 480// * 'bucket.split'. 481// */ 482// split[i] = split[i] + " "; 483 final String chunk = split[i]; 484 final int chunklen = chunk.length(); 485 final boolean addLeadingSpace = 0 < curlen && 0 < i; 486 if (curlen + chunklen + (addLeadingSpace ? 1 : 0) <= width) { 487 if (addLeadingSpace) { 488 /* 489 * Do not forget space on which chunk got split. If 490 * the space is offscreen, it's harmless, hence not 491 * checking it. 492 */ 493 current.append(' ', null); 494 curlen++; 495 } 496 497 /* Can add it */ 498 current.append(chunk, color); 499 /* Extend size */ 500 curlen += chunklen; 501 } else { 502 /* Need to wrap */ 503 /* Flush content so far */ 504 if (!current.isEmpty()) 505 buf.add(current); 506 /* 507 * else: line was prepared, but did not contain anything 508 */ 509 if (chunklen <= width) { 510 current = create(); 511 current.append(chunk, color); 512 /* Reinit size */ 513 curlen = chunklen; 514 } else { 515 /* 516 * This word is too long. Adding it and preparing a 517 * new line immediately. 518 */ 519 /* Add */ 520 buf.add(new Impl<>(chunk, color)); 521 /* Prepare for next rolls */ 522 current = create(); 523 /* Reinit size */ 524 curlen = 0; 525 } 526 } 527 } 528 } 529 530 if (!current.isEmpty()) { 531 /* Flush rest */ 532 buf.add(current); 533 } 534 535 return buf; 536 } 537 538 @Override 539 /* 540 * smelC: not the cutest result (we should add spaces both from the left 541 * and the right, instead of just from the left), but better than 542 * nothing. 543 */ 544 public IColoredString<T> justify(int width) { 545 int length = length(); 546 547 if (width <= length) 548 /* 549 * If width==length, we're good. If width<length, we cannot 550 * adjust 551 */ 552 return this; 553 554 int totalDiff = width - length; 555 assert 0 < totalDiff; 556 557 if (width <= totalDiff * 3) 558 /* Too much of a difference, it would look very weird. */ 559 return this; 560 561 final IColoredString.Impl<T> result = create(); 562 563 ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator(); 564 final int nbb = fragments.size(); 565 final int[] bucketToNbSpaces = new int[nbb]; 566 /* The number of buckets that can contribute to justifying */ 567 int totalNbSpaces = 0; 568 /* The index of the last bucket that has spaces */ 569 int lastHopeIndex = -1; 570 { 571 int i = 0; 572 while (it.hasNext()) { 573 final Bucket<T> next = it.next(); 574 final int nbs = nbSpaces(next.getText()); 575 totalNbSpaces += nbs; 576 bucketToNbSpaces[i] = nbs; 577 i++; 578 } 579 580 if (totalNbSpaces == 0) 581 /* Cannot do anything */ 582 return this; 583 584 for (int j = bucketToNbSpaces.length - 1; 0 <= j; j--) { 585 if (0 < bucketToNbSpaces[j]) { 586 lastHopeIndex = j; 587 break; 588 } 589 } 590 /* Holds because we ruled out 'totalNbSpaces == 0' before */ 591 assert 0 <= lastHopeIndex; 592 } 593 594 // we know totalNbSpaces cannot be 0 from prior checks, so division is OK 595 int toAddPerSpace = (totalDiff / totalNbSpaces); 596 int totalRest = totalDiff - (toAddPerSpace * totalNbSpaces); 597 assert 0 <= totalRest; 598 599 int bidx = -1; 600 601 it = fragments.listIterator(); 602 603 while (it.hasNext() && 0 < totalDiff) { 604 bidx++; 605 final Bucket<T> next = it.next(); 606 final String bucket = next.getText(); 607 final int blength = bucket.length(); 608 final int localNbSpaces = bucketToNbSpaces[bidx]; 609 if (localNbSpaces == 0) { 610 /* Cannot change it */ 611 result.append(next); 612 continue; 613 } 614 int localDiff = localNbSpaces * toAddPerSpace; 615 assert localDiff <= totalDiff; 616 int nb = localDiff / localNbSpaces; 617 int localRest = localDiff - (nb * localNbSpaces); 618 if (localRest == 0 && 0 < totalRest) { 619 /* 620 * Take one for the group. This avoids flushing all spaces 621 * needed in the 'last hope' cases below. 622 */ 623 localRest = 1; 624 } 625 assert 0 <= localRest; 626 assert localRest <= totalRest; 627 StringBuilder novel = new StringBuilder(); 628 int eatenSpaces = 1; 629 for (int i = 0; i < blength; i++) { 630 final char c = bucket.charAt(i); 631 novel.append(c); 632 if (c == ' ' && (0 < localDiff || 0 < totalDiff || 0 < localRest || 0 < totalRest)) { 633 /* Can (and should) add an extra space */ 634 for (int j = 0; j < nb && 0 < localDiff; j++) { 635 novel.append(" "); 636 localDiff--; 637 totalDiff--; 638 } 639 if (0 < localRest || 0 < totalRest) { 640 if (eatenSpaces == localNbSpaces) { 641 /* I'm the last hope for this bucket */ 642 for (int j = 0; j < localRest; j++) { 643 novel.append(" "); 644 localRest--; 645 totalRest--; 646 } 647 if (bidx == lastHopeIndex) { 648 /* I'm the last hope globally */ 649 while (0 < totalRest) { 650 novel.append(" "); 651 totalRest--; 652 } 653 } 654 } else { 655 if (0 < localRest && 0 < totalRest) { 656 /* Not the last hope: take one only */ 657 novel.append(" "); 658 localRest--; 659 totalRest--; 660 } 661 } 662 } 663 eatenSpaces++; 664 } 665 } 666 /* I did my job */ 667 assert localRest == 0; 668 /* If I was the hope, I did my job */ 669 assert bidx != lastHopeIndex || totalRest == 0; 670 result.append(novel.toString(), next.getColor()); 671 } 672 673 while (it.hasNext()) 674 result.append(it.next()); 675 676 return result; 677 } 678 679 @Override 680 public void clear() { 681 fragments.clear(); 682 } 683 684 @Override 685 public int length() { 686 int result = 0; 687 for (Bucket<T> fragment : fragments) 688 result += fragment.getText().length(); 689 return result; 690 } 691 692 @Override 693 /* This implementation is resilient to empty buckets */ 694 public boolean isEmpty() { 695 for (Bucket<?> bucket : fragments) { 696 if (bucket.text != null && !bucket.text.isEmpty()) { 697 return false; 698 } 699 } 700 return true; 701 } 702 703 @Override 704 public T lastColor() { 705 return fragments.isEmpty() ? null : fragments.get(fragments.size() - 1).color; 706 } 707 708 @Override 709 public T colorAt(int index) { 710 final ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator(); 711 int now = 0; 712 while (it.hasNext()) { 713 final IColoredString.Bucket<T> next = it.next(); 714 final String ftext = next.text; 715 final int flen = ftext.length(); 716 final int nextl = now + flen; 717 if (index < nextl) 718 return next.color; 719 now += flen; 720 } 721 throw new NoSuchElementException("Color at index " + index + " in " + this); 722 } 723 724 @Override 725 public char charAt(int index) { 726 final ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator(); 727 int now = 0; 728 while (it.hasNext()) { 729 final IColoredString.Bucket<T> next = it.next(); 730 final String ftext = next.text; 731 final int flen = ftext.length(); 732 final int nextl = now + flen; 733 if (index < nextl) 734 return ftext.charAt(index - now); 735 now += flen; 736 } 737 throw new NoSuchElementException("Character at index " + index + " in " + this); 738 } 739 740 @Override 741 public String present() { 742 final StringBuilder result = new StringBuilder(); 743 for (Bucket<T> fragment : fragments) 744 result.append(fragment.text); 745 return result.toString(); 746 } 747 /** 748 * Given some way of converting from a T value to an in-line markup tag, returns a string representation of 749 * this IColoredString with in-line markup representing colors. 750 * @param markup an IMarkup implementation 751 * @return a String with markup inserted inside. 752 */ 753 @Override 754 public String presentWithMarkup(IMarkup<T> markup) { 755 final StringBuilder result = new StringBuilder(); 756// boolean open = false; 757 for (Bucket<T> fragment : fragments) { 758 if(fragment.color != null) { 759 // extra close-markup tags can cause variable-width text to have huge spacing. 760 // they shouldn't be needed anyway. 761// if (open) 762// result.append(markup.closeMarkup()); 763 result.append(markup.getMarkup(fragment.color)); 764// open = true; 765 } 766// else { 767//// if (open) 768//// result.append(markup.closeMarkup()); 769// open = false; 770// } 771 // maybe try this line if escape() is re-added to IMarkup 772 //result.append(markup.escape(fragment.text)); 773 result.append(fragment.text); 774 } 775 return result.toString(); 776 } 777 778 @Override 779 public ListIterator<Bucket<T>> iterator() { 780 return fragments.listIterator(); 781 } 782 783 @Override 784 public String toString() { 785 return present(); 786 } 787 788 protected static boolean equals(Object o1, Object o2) { 789 if (o1 == null) 790 return o2 == null; 791 else 792 return o1.equals(o2); 793 } 794 795 private int nbSpaces(String s) { 796 final int bd = s.length(); 797 int result = 0; 798 for (int i = 0; i < bd; i++) { 799 final char c = s.charAt(i); 800 if (c == ' ') 801 result++; 802 } 803 return result; 804 } 805 /** 806 * Gets the Buckets as an ArrayList, allowing access by index instead of by {@link #iterator()}. 807 * 808 * @return the Buckets that would be returned by {@link #iterator()}, but in an ArrayList. 809 */ 810 @Override 811 public ArrayList<Bucket<T>> getFragments() { 812 return fragments; 813 } 814 815 /* Some tests */ 816 /* 817 public static void main(String[] args) { 818 final IColoredString<Object> rockNRoll = IColoredString.Impl.create(); 819 rockNRoll.append("Rock", new Object()); 820 rockNRoll.append(" ", new Object()); 821 rockNRoll.append("'n", new Object()); 822 rockNRoll.append(" ", new Object()); 823 rockNRoll.append("Roll", new Object()); 824 for (int i = 0; i < rockNRoll.length(); i++) 825 System.out.println(rockNRoll.charAt(i)); 826 System.out.println(rockNRoll.present()); 827 } 828 */ 829 } 830 831 /** 832 * A piece of a {@link IColoredString}: a text and its color. 833 * 834 * @author smelC 835 * 836 * @param <T> 837 * The type of colors; 838 */ 839 class Bucket<T> { 840 841 protected final String text; 842 protected final/* @Nullable */T color; 843 844 public Bucket(String text, /* @Nullable */T color) { 845 this.text = text == null ? "" : text; 846 this.color = color; 847 } 848 849 /** 850 * @param text 851 * @return An instance whose text is {@code this.text + text}. Color is 852 * unchanged. 853 */ 854 public Bucket<T> append(String text) { 855 if (text == null || text.isEmpty()) 856 /* Let's save an allocation */ 857 return this; 858 else 859 return new Bucket<>(this.text + text, color); 860 } 861 862 public int length() 863 { 864 return text.length(); 865 } 866 867 public Bucket<T> setLength(int l) { 868 final int here = text.length(); 869 if (here <= l) 870 return this; 871 else 872 return new Bucket<>(text.substring(0, l), color); 873 } 874 875 public Bucket<T> setColor(T t) { 876 return color == t ? this : new Bucket<T>(text, t); 877 } 878 879 /** 880 * @return The text that this bucket contains. 881 */ 882 public String getText() { 883 return text; 884 } 885 886 /** 887 * @return The color of {@link #getText()}. Or {@code null} if none. 888 */ 889 public/* @Nullable */T getColor() { 890 return color; 891 } 892 893 @Override 894 public String toString() { 895 if (color == null) 896 return text; 897 else 898 return text + '(' + color + ')'; 899 } 900 901 } 902}