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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 }
--- EOF ---