1 /* 2 * Copyright (c) 1998, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 /* 27 * (C) Copyright Taligent, Inc. 1996 - 1997, All Rights Reserved 28 * (C) Copyright IBM Corp. 1996 - 1998, All Rights Reserved 29 * 30 * The original version of this source code and documentation is 31 * copyrighted and owned by Taligent, Inc., a wholly-owned subsidiary 32 * of IBM. These materials are provided under terms of a License 33 * Agreement between Taligent and Sun. This technology is protected 34 * by multiple US and International patents. 35 * 36 * This notice and attribution to Taligent may not be removed. 37 * Taligent is a registered trademark of Taligent, Inc. 38 * 39 */ 40 41 package java.awt.font; 42 43 import java.text.BreakIterator; 44 import java.text.CharacterIterator; 45 import java.text.AttributedCharacterIterator; 46 import java.awt.font.FontRenderContext; 47 48 /** 49 * The {@code LineBreakMeasurer} class allows styled text to be 50 * broken into lines (or segments) that fit within a particular visual 51 * advance. This is useful for clients who wish to display a paragraph of 52 * text that fits within a specific width, called the <b>wrapping 53 * width</b>. 54 * <p> 55 * {@code LineBreakMeasurer} is constructed with an iterator over 56 * styled text. The iterator's range should be a single paragraph in the 57 * text. 58 * {@code LineBreakMeasurer} maintains a position in the text for the 59 * start of the next text segment. Initially, this position is the 60 * start of text. Paragraphs are assigned an overall direction (either 61 * left-to-right or right-to-left) according to the bidirectional 62 * formatting rules. All segments obtained from a paragraph have the 63 * same direction as the paragraph. 64 * <p> 65 * Segments of text are obtained by calling the method 66 * {@code nextLayout}, which returns a {@link TextLayout} 67 * representing the text that fits within the wrapping width. 68 * The {@code nextLayout} method moves the current position 69 * to the end of the layout returned from {@code nextLayout}. 70 * <p> 71 * {@code LineBreakMeasurer} implements the most commonly used 72 * line-breaking policy: Every word that fits within the wrapping 73 * width is placed on the line. If the first word does not fit, then all 74 * of the characters that fit within the wrapping width are placed on the 75 * line. At least one character is placed on each line. 76 * <p> 77 * The {@code TextLayout} instances returned by 78 * {@code LineBreakMeasurer} treat tabs like 0-width spaces. Clients 79 * who wish to obtain tab-delimited segments for positioning should use 80 * the overload of {@code nextLayout} which takes a limiting offset 81 * in the text. 82 * The limiting offset should be the first character after the tab. 83 * The {@code TextLayout} objects returned from this method end 84 * at the limit provided (or before, if the text between the current 85 * position and the limit won't fit entirely within the wrapping 86 * width). 87 * <p> 88 * Clients who are laying out tab-delimited text need a slightly 89 * different line-breaking policy after the first segment has been 90 * placed on a line. Instead of fitting partial words in the 91 * remaining space, they should place words which don't fit in the 92 * remaining space entirely on the next line. This change of policy 93 * can be requested in the overload of {@code nextLayout} which 94 * takes a {@code boolean} parameter. If this parameter is 95 * {@code true}, {@code nextLayout} returns 96 * {@code null} if the first word won't fit in 97 * the given space. See the tab sample below. 98 * <p> 99 * In general, if the text used to construct the 100 * {@code LineBreakMeasurer} changes, a new 101 * {@code LineBreakMeasurer} must be constructed to reflect 102 * the change. (The old {@code LineBreakMeasurer} continues to 103 * function properly, but it won't be aware of the text change.) 104 * Nevertheless, if the text change is the insertion or deletion of a 105 * single character, an existing {@code LineBreakMeasurer} can be 106 * 'updated' by calling {@code insertChar} or 107 * {@code deleteChar}. Updating an existing 108 * {@code LineBreakMeasurer} is much faster than creating a new one. 109 * Clients who modify text based on user typing should take advantage 110 * of these methods. 111 * <p> 112 * <strong>Examples</strong>:<p> 113 * Rendering a paragraph in a component 114 * <blockquote> 115 * <pre>{@code 116 * public void paint(Graphics graphics) { 117 * 118 * float dx = 0f, dy = 5f; 119 * Graphics2D g2d = (Graphics2D)graphics; 120 * FontRenderContext frc = g2d.getFontRenderContext(); 121 * 122 * AttributedString text = new AttributedString("....."); 123 * AttributedCharacterIterator paragraph = text.getIterator(); 124 * 125 * LineBreakMeasurer measurer = new LineBreakMeasurer(paragraph, frc); 126 * measurer.setPosition(paragraph.getBeginIndex()); 127 * float wrappingWidth = (float)getSize().width; 128 * 129 * while (measurer.getPosition() < paragraph.getEndIndex()) { 130 * 131 * TextLayout layout = measurer.nextLayout(wrappingWidth); 132 * 133 * dy += (layout.getAscent()); 134 * float dx = layout.isLeftToRight() ? 135 * 0 : (wrappingWidth - layout.getAdvance()); 136 * 137 * layout.draw(graphics, dx, dy); 138 * dy += layout.getDescent() + layout.getLeading(); 139 * } 140 * } 141 * }</pre> 142 * </blockquote> 143 * <p> 144 * Rendering text with tabs. For simplicity, the overall text 145 * direction is assumed to be left-to-right 146 * <blockquote> 147 * <pre>{@code 148 * public void paint(Graphics graphics) { 149 * 150 * float leftMargin = 10, rightMargin = 310; 151 * float[] tabStops = { 100, 250 }; 152 * 153 * // assume styledText is an AttributedCharacterIterator, and the number 154 * // of tabs in styledText is tabCount 155 * 156 * int[] tabLocations = new int[tabCount+1]; 157 * 158 * int i = 0; 159 * for (char c = styledText.first(); c != styledText.DONE; c = styledText.next()) { 160 * if (c == '\t') { 161 * tabLocations[i++] = styledText.getIndex(); 162 * } 163 * } 164 * tabLocations[tabCount] = styledText.getEndIndex() - 1; 165 * 166 * // Now tabLocations has an entry for every tab's offset in 167 * // the text. For convenience, the last entry is tabLocations 168 * // is the offset of the last character in the text. 169 * 170 * LineBreakMeasurer measurer = new LineBreakMeasurer(styledText); 171 * int currentTab = 0; 172 * float verticalPos = 20; 173 * 174 * while (measurer.getPosition() < styledText.getEndIndex()) { 175 * 176 * // Lay out and draw each line. All segments on a line 177 * // must be computed before any drawing can occur, since 178 * // we must know the largest ascent on the line. 179 * // TextLayouts are computed and stored in a Vector; 180 * // their horizontal positions are stored in a parallel 181 * // Vector. 182 * 183 * // lineContainsText is true after first segment is drawn 184 * boolean lineContainsText = false; 185 * boolean lineComplete = false; 186 * float maxAscent = 0, maxDescent = 0; 187 * float horizontalPos = leftMargin; 188 * Vector layouts = new Vector(1); 189 * Vector penPositions = new Vector(1); 190 * 191 * while (!lineComplete) { 192 * float wrappingWidth = rightMargin - horizontalPos; 193 * TextLayout layout = 194 * measurer.nextLayout(wrappingWidth, 195 * tabLocations[currentTab]+1, 196 * lineContainsText); 197 * 198 * // layout can be null if lineContainsText is true 199 * if (layout != null) { 200 * layouts.addElement(layout); 201 * penPositions.addElement(new Float(horizontalPos)); 202 * horizontalPos += layout.getAdvance(); 203 * maxAscent = Math.max(maxAscent, layout.getAscent()); 204 * maxDescent = Math.max(maxDescent, 205 * layout.getDescent() + layout.getLeading()); 206 * } else { 207 * lineComplete = true; 208 * } 209 * 210 * lineContainsText = true; 211 * 212 * if (measurer.getPosition() == tabLocations[currentTab]+1) { 213 * currentTab++; 214 * } 215 * 216 * if (measurer.getPosition() == styledText.getEndIndex()) 217 * lineComplete = true; 218 * else if (horizontalPos >= tabStops[tabStops.length-1]) 219 * lineComplete = true; 220 * 221 * if (!lineComplete) { 222 * // move to next tab stop 223 * int j; 224 * for (j=0; horizontalPos >= tabStops[j]; j++) {} 225 * horizontalPos = tabStops[j]; 226 * } 227 * } 228 * 229 * verticalPos += maxAscent; 230 * 231 * Enumeration layoutEnum = layouts.elements(); 232 * Enumeration positionEnum = penPositions.elements(); 233 * 234 * // now iterate through layouts and draw them 235 * while (layoutEnum.hasMoreElements()) { 236 * TextLayout nextLayout = (TextLayout) layoutEnum.nextElement(); 237 * Float nextPosition = (Float) positionEnum.nextElement(); 238 * nextLayout.draw(graphics, nextPosition.floatValue(), verticalPos); 239 * } 240 * 241 * verticalPos += maxDescent; 242 * } 243 * } 244 * }</pre> 245 * </blockquote> 246 * @see TextLayout 247 */ 248 249 public final class LineBreakMeasurer { 250 251 private BreakIterator breakIter; 252 private int start; 253 private int pos; 254 private int limit; 255 private TextMeasurer measurer; 256 private CharArrayIterator charIter; 257 258 /** 259 * Constructs a {@code LineBreakMeasurer} for the specified text. 260 * 261 * @param text the text for which this {@code LineBreakMeasurer} 262 * produces {@code TextLayout} objects; the text must contain 263 * at least one character; if the text available through 264 * {@code iter} changes, further calls to this 265 * {@code LineBreakMeasurer} instance are undefined (except, 266 * in some cases, when {@code insertChar} or 267 * {@code deleteChar} are invoked afterward - see below) 268 * @param frc contains information about a graphics device which is 269 * needed to measure the text correctly; 270 * text measurements can vary slightly depending on the 271 * device resolution, and attributes such as antialiasing; this 272 * parameter does not specify a translation between the 273 * {@code LineBreakMeasurer} and user space 274 * @see LineBreakMeasurer#insertChar 275 * @see LineBreakMeasurer#deleteChar 276 */ LineBreakMeasurer(AttributedCharacterIterator text, FontRenderContext frc)277 public LineBreakMeasurer(AttributedCharacterIterator text, FontRenderContext frc) { 278 this(text, BreakIterator.getLineInstance(), frc); 279 } 280 281 /** 282 * Constructs a {@code LineBreakMeasurer} for the specified text. 283 * 284 * @param text the text for which this {@code LineBreakMeasurer} 285 * produces {@code TextLayout} objects; the text must contain 286 * at least one character; if the text available through 287 * {@code iter} changes, further calls to this 288 * {@code LineBreakMeasurer} instance are undefined (except, 289 * in some cases, when {@code insertChar} or 290 * {@code deleteChar} are invoked afterward - see below) 291 * @param breakIter the {@link BreakIterator} which defines line 292 * breaks 293 * @param frc contains information about a graphics device which is 294 * needed to measure the text correctly; 295 * text measurements can vary slightly depending on the 296 * device resolution, and attributes such as antialiasing; this 297 * parameter does not specify a translation between the 298 * {@code LineBreakMeasurer} and user space 299 * @throws IllegalArgumentException if the text has less than one character 300 * @see LineBreakMeasurer#insertChar 301 * @see LineBreakMeasurer#deleteChar 302 */ LineBreakMeasurer(AttributedCharacterIterator text, BreakIterator breakIter, FontRenderContext frc)303 public LineBreakMeasurer(AttributedCharacterIterator text, 304 BreakIterator breakIter, 305 FontRenderContext frc) { 306 if (text.getEndIndex() - text.getBeginIndex() < 1) { 307 throw new IllegalArgumentException("Text must contain at least one character."); 308 } 309 310 this.breakIter = breakIter; 311 this.measurer = new TextMeasurer(text, frc); 312 this.limit = text.getEndIndex(); 313 this.pos = this.start = text.getBeginIndex(); 314 315 charIter = new CharArrayIterator(measurer.getChars(), this.start); 316 this.breakIter.setText(charIter); 317 } 318 319 /** 320 * Returns the position at the end of the next layout. Does NOT 321 * update the current position of this {@code LineBreakMeasurer}. 322 * 323 * @param wrappingWidth the maximum visible advance permitted for 324 * the text in the next layout 325 * @return an offset in the text representing the limit of the 326 * next {@code TextLayout}. 327 */ nextOffset(float wrappingWidth)328 public int nextOffset(float wrappingWidth) { 329 return nextOffset(wrappingWidth, limit, false); 330 } 331 332 /** 333 * Returns the position at the end of the next layout. Does NOT 334 * update the current position of this {@code LineBreakMeasurer}. 335 * 336 * @param wrappingWidth the maximum visible advance permitted for 337 * the text in the next layout 338 * @param offsetLimit the first character that can not be included 339 * in the next layout, even if the text after the limit would fit 340 * within the wrapping width; {@code offsetLimit} must be 341 * greater than the current position 342 * @param requireNextWord if {@code true}, the current position 343 * that is returned if the entire next word does not fit within 344 * {@code wrappingWidth}; if {@code false}, the offset 345 * returned is at least one greater than the current position 346 * @return an offset in the text representing the limit of the 347 * next {@code TextLayout} 348 */ nextOffset(float wrappingWidth, int offsetLimit, boolean requireNextWord)349 public int nextOffset(float wrappingWidth, int offsetLimit, 350 boolean requireNextWord) { 351 352 int nextOffset = pos; 353 354 if (pos < limit) { 355 if (offsetLimit <= pos) { 356 throw new IllegalArgumentException("offsetLimit must be after current position"); 357 } 358 359 int charAtMaxAdvance = 360 measurer.getLineBreakIndex(pos, wrappingWidth); 361 362 if (charAtMaxAdvance == limit) { 363 nextOffset = limit; 364 } 365 else if (Character.isWhitespace(measurer.getChars()[charAtMaxAdvance-start])) { 366 nextOffset = breakIter.following(charAtMaxAdvance); 367 } 368 else { 369 // Break is in a word; back up to previous break. 370 371 // NOTE: I think that breakIter.preceding(limit) should be 372 // equivalent to breakIter.last(), breakIter.previous() but 373 // the authors of BreakIterator thought otherwise... 374 // If they were equivalent then the first branch would be 375 // unnecessary. 376 int testPos = charAtMaxAdvance + 1; 377 if (testPos == limit) { 378 breakIter.last(); 379 nextOffset = breakIter.previous(); 380 } 381 else { 382 nextOffset = breakIter.preceding(testPos); 383 } 384 385 if (nextOffset <= pos) { 386 // first word doesn't fit on line 387 if (requireNextWord) { 388 nextOffset = pos; 389 } 390 else { 391 nextOffset = Math.max(pos+1, charAtMaxAdvance); 392 } 393 } 394 } 395 } 396 397 if (nextOffset > offsetLimit) { 398 nextOffset = offsetLimit; 399 } 400 401 return nextOffset; 402 } 403 404 /** 405 * Returns the next layout, and updates the current position. 406 * 407 * @param wrappingWidth the maximum visible advance permitted for 408 * the text in the next layout 409 * @return a {@code TextLayout}, beginning at the current 410 * position, which represents the next line fitting within 411 * {@code wrappingWidth} 412 */ nextLayout(float wrappingWidth)413 public TextLayout nextLayout(float wrappingWidth) { 414 return nextLayout(wrappingWidth, limit, false); 415 } 416 417 /** 418 * Returns the next layout, and updates the current position. 419 * 420 * @param wrappingWidth the maximum visible advance permitted 421 * for the text in the next layout 422 * @param offsetLimit the first character that can not be 423 * included in the next layout, even if the text after the limit 424 * would fit within the wrapping width; {@code offsetLimit} 425 * must be greater than the current position 426 * @param requireNextWord if {@code true}, and if the entire word 427 * at the current position does not fit within the wrapping width, 428 * {@code null} is returned. If {@code false}, a valid 429 * layout is returned that includes at least the character at the 430 * current position 431 * @return a {@code TextLayout}, beginning at the current 432 * position, that represents the next line fitting within 433 * {@code wrappingWidth}. If the current position is at the end 434 * of the text used by this {@code LineBreakMeasurer}, 435 * {@code null} is returned 436 */ nextLayout(float wrappingWidth, int offsetLimit, boolean requireNextWord)437 public TextLayout nextLayout(float wrappingWidth, int offsetLimit, 438 boolean requireNextWord) { 439 440 if (pos < limit) { 441 int layoutLimit = nextOffset(wrappingWidth, offsetLimit, requireNextWord); 442 if (layoutLimit == pos) { 443 return null; 444 } 445 446 TextLayout result = measurer.getLayout(pos, layoutLimit); 447 pos = layoutLimit; 448 449 return result; 450 } else { 451 return null; 452 } 453 } 454 455 /** 456 * Returns the current position of this {@code LineBreakMeasurer}. 457 * 458 * @return the current position of this {@code LineBreakMeasurer} 459 * @see #setPosition 460 */ getPosition()461 public int getPosition() { 462 return pos; 463 } 464 465 /** 466 * Sets the current position of this {@code LineBreakMeasurer}. 467 * 468 * @param newPosition the current position of this 469 * {@code LineBreakMeasurer}; the position should be within the 470 * text used to construct this {@code LineBreakMeasurer} (or in 471 * the text most recently passed to {@code insertChar} 472 * or {@code deleteChar} 473 * @see #getPosition 474 */ setPosition(int newPosition)475 public void setPosition(int newPosition) { 476 if (newPosition < start || newPosition > limit) { 477 throw new IllegalArgumentException("position is out of range"); 478 } 479 pos = newPosition; 480 } 481 482 /** 483 * Updates this {@code LineBreakMeasurer} after a single 484 * character is inserted into the text, and sets the current 485 * position to the beginning of the paragraph. 486 * 487 * @param newParagraph the text after the insertion 488 * @param insertPos the position in the text at which the character 489 * is inserted 490 * @throws IndexOutOfBoundsException if {@code insertPos} is less 491 * than the start of {@code newParagraph} or greater than 492 * or equal to the end of {@code newParagraph} 493 * @throws NullPointerException if {@code newParagraph} is 494 * {@code null} 495 * @see #deleteChar 496 */ insertChar(AttributedCharacterIterator newParagraph, int insertPos)497 public void insertChar(AttributedCharacterIterator newParagraph, 498 int insertPos) { 499 500 measurer.insertChar(newParagraph, insertPos); 501 502 limit = newParagraph.getEndIndex(); 503 pos = start = newParagraph.getBeginIndex(); 504 505 charIter.reset(measurer.getChars(), newParagraph.getBeginIndex()); 506 breakIter.setText(charIter); 507 } 508 509 /** 510 * Updates this {@code LineBreakMeasurer} after a single 511 * character is deleted from the text, and sets the current 512 * position to the beginning of the paragraph. 513 * @param newParagraph the text after the deletion 514 * @param deletePos the position in the text at which the character 515 * is deleted 516 * @throws IndexOutOfBoundsException if {@code deletePos} is 517 * less than the start of {@code newParagraph} or greater 518 * than the end of {@code newParagraph} 519 * @throws NullPointerException if {@code newParagraph} is 520 * {@code null} 521 * @see #insertChar 522 */ deleteChar(AttributedCharacterIterator newParagraph, int deletePos)523 public void deleteChar(AttributedCharacterIterator newParagraph, 524 int deletePos) { 525 526 measurer.deleteChar(newParagraph, deletePos); 527 528 limit = newParagraph.getEndIndex(); 529 pos = start = newParagraph.getBeginIndex(); 530 531 charIter.reset(measurer.getChars(), start); 532 breakIter.setText(charIter); 533 } 534 } 535