The
java.awt.font.TextLayout
object lets you draw styled
text in any language or script supported by
The Unicode
Standard--a global character coding system for
handling diverse modern, classical, and historical languages.
When drawing text, the direction the text is read must be taken
into account so all words in the the string display correctly.
A TextLayout
object maintains the direction of the
text and correctly draws it no matter if the string runs
left-to-right, right-to-left, or both (bidirectional).
Arabic and Hebrew are bidirectional because their text runs
right-to-left and their numbers run left-to-right. Also, any
string with embedded text that runs in the opposite direction
from the main text (English with embedded Arabic text, for example),
is bidirectional.
Bidirectional text presents interesting problems for correctly
positioning carets, accurately locating selections, and correctly
displaying multiple lines. Also bidirectional and right-to-left
text present similar problems for moving the caret in the correct
direction in response to right and left arrow key presses.
This lesson describes these issues and demonstrates how a
TextLayout
object takes care of them. See the
International
Text in JDK 1.2 paper for more details on the
information presented in this lesson.
About the Examples
The following examples support foreign language text with
the -text
option. Valid -text
values are
hebrew, english, mixed, arabic, longenglish, and longhebrew.
To see the foreign language text, you need to install a
unicode font like Bitstream
Cyberbit on your system.
For example, with a unicode font set up on your system,
you can start the DrawSample application as follows to see a text string
in Hebrew:
java DrawSample -text hebrew
The
SampleUtils.java
source code provides
text in various languages and other things used by the example applications.
Inserting Text
In editable text, a caret displays where the end user clicks to
indicate the insertion point where the end user will enter text.
When the insertion point falls between right-to-left text like
Arabic and left-to-right text like English, the same character location
in the source text (shown on top) maps to two insertion points on the
display (shown on the bottom). One location
is the insertion point for English text and the other location
is the insertion point for Arabic text. In the figure,
character location 8 in the source text maps to the space after the word
is or the first character in the right-to-left Arabic text in the
displayed text.
Note:
The Arabic text is in bold capitalized English to help people
who do not read Arabic understand the point better.
The TextLayout
object ensures the inserted text appears in the
correct location on the display based on which character was hit, the side on
which the characer was hit, and the language the end user enters.
The next two figures display the same text, which consists of mainly
right-to-left text with two left-to-right words (Hello and Arabic)
embedded. When the end user clicks on the o in Hello
or on the space after the o, dual carets display.
Dual carets consist of
a strong and weak caret, and in the figures, the strong caret is red
and the weak caret is black. The carets represent
boundaries between glyphs for selection highlighting, hit testing, and
moving the caret with arrow keys. The TextLayout
object
draws dual carets because the end user clicked on a directional
boundary where right-to-left text meets left-to-right text. If the
end user had clicked on a non-directional boundary, the
TextLayout
object would have drawn a single caret at
that location.
Note: If you do not want to use
dual carets, you can extend the TextLayout.CaretPolicy
class to use something other than dual carets to mark directional
boundaries.
Character Hit, Side, and Language
A click on the o on the side of the o towards the Hebrew
records that the end user clicked after the o, which is
part of the English. This positions the weak (black) caret next to
the o and the strong caret (red) in front of the H.
If the end user enters English, it appears after the
o, and if the end user enters Hebrew, it appears before the
H.
A click on the space to the right of the o records that the end
user clicked the space, which is part of the Hebrew. This positions
the strong (red) caret next to the o and the weak caret (black)
in front of the H. If the end user types English,
it appears before the H, and if the end user types Hebrew,
it appears after the o.
Note:
The insertion offset is the nearest one in the text. If it is off one
end of the line, the offset at that end is returned.
Caret Positioning
You might be wondering why the caret positions do not include the spaces
on either side.
Spaces are either left-to-right or right-to-left characters depending on what
is next to them. If the characters on both sides of a space are the same
kind of character, the space is that kind of character too. Spaces between
Arabic words are treated like Arabic characters, and spaces between English
words are treated like English characters. When the characters on both sides
are different, spaces are treated like the overall direction of the paragraph:
If the paragraph as a whole is left-to-right, the space is left-to-right, and
if the paragraph as a whole is right-to-left, the space is right-to-left.
In the Hit Test sample, the overall text is right-to-left. The spaces on each
side of Hello each have one neighbor that is left-to-right (the English)
and one that is right-to-left (the Hebrew). Because the text is right-to-left,
the spaces are right-to-left too, and the split carets appear next to the o
and H because the spaces being right-to-left belong to the right-to-left
text on either side.
Hit Testing
In code, a point returned by a mouse click is passed to the
TextLayout.hitTestChar
method, which returns a
TextHitInfo
object that represents the character and side of the
character where the end user clicked. If the end user clicks on the o,
the hit is to position 5.
However, in the source text, position 5 is before the H, so
the TextLayout
object checks the TextHitInfo
object to find out what side of the character the hit is on and displays
the dual carets if the hit is on the side of the o towards
the Hebrew. If the hit is on the side of the o towards the
l, a single caret is drawn between the l and the
o to indicate that the end user will insert English text
at that location.
The source text is initialized the way the words are spoken, and
not the way they are printed. The source text looks like this:
"\u05D0\u05E0\u05D9 Hello
\u05DC\u05D0 \u05DE\u05D1\u05D9\ u05DF " +
"\u05E2\u05D1\u05E8\u05D9\u05EA Arabic
\u0644\u0645\u062C\ u0645\u0648\u0639\u0629", map);
The first three unicode characters and the space \u05D0\u05E0\u05D9
define what you see on the display starting on the right side up to the o.
You can see that the H is next in the source code, but the o is
next on the display. This is because in the right-to-left run, the English
word Hello is turned around to display correctly in an embedded
left-to-right run.
Determining the location of the insertion point is taken care of
by the TextLayout
object. All you need to do in your
code is specify the caret colors, get the mouse click location, return
a TextHitInfo
object, and draw the layout and carets.
Here is the code to initialize the colors for the strong
and weak carets:
private static final Color
STRONG_CARET_COLOR = Color.red;
private static final Color
WEAK_CARET_COLOR = Color.black;
Here is the code that draws the TextLayout
and
the carets. The insertionPoint
variable is
retrieved from a TextHitInfo
object in the
HitTestMouseListener
method.
If the insertion point is not between text running in different
directions, only the strong caret draws.
// Draw textLayout.
textLayout.draw(graphics2D, 0, 0);
// Retrieve caret Shapes for insertionIndex.
Shape[] carets =
textLayout.getCaretShapes(insertionIndex);
// Draw the carets. carets[0] is the strong caret, and
// is never null. carets[1], if it is not null, is the
// weak caret.
graphics2D.setColor(STRONG_CARET_COLOR);
graphics2D.draw(carets[0]);
if (carets[1] != null) {
graphics2D.setColor(WEAK_CARET_COLOR);
graphics2D.draw(carets[1]);
}
Here is the HitTestMouseListener
method:
private class HitTestMouseListener
extends MouseAdapter {
/**
* Compute the character position of the mouse click.
*/
public void mouseClicked(MouseEvent e) {
Point2D origin = computeLayoutOrigin();
// Compute the mouse click location relative to
// textLayout's origin.
float clickX = (float) (e.getX() - origin.getX());
float clickY = (float) (e.getY() - origin.getY());
// Get the character position of the mouse click.
TextHitInfo currentHit =
textLayout.hitTestChar(clickX, clickY);
insertionIndex = currentHit.getInsertionIndex();
// Repaint the Component so the new caret(s)
// will be displayed.
repaint();
}
}
And here is the complete HitTestSample.java
source code.
Selection Highlighting
The next figure shows how a
contiguous range of characters in the source text (on the top)
might not map to a contiguous highlight region (on the bottom)
on screen if the selection range includes left-to-right and right-to-left
characters. When the Arabic text is turned around to run right-to-left on
the display, the selected portion of the Arabic text is not contiguous
with the is and space before it, althout these characters
are contiguous in the source text.
The next figure shows how a contiguous highlight region on the
display (on the bottom) might not map to a single contiguous range
of characters in the source text (on the top).
This point is illustrated in the next figure.
A TextLayout
object provides two strategies for selection
highlighting to handle these two situations: logical highlighting and visual
highlighting.
With logical highlighting, the selected characters are always contiguous
in the source text, and the highlight region is allowed to be discontiguous
on the display. With visual highlighting, there might be more than one
range of selected characters, but the highlight region is always contiguous.
Logical highlighting is simpler for programmers to use because the selected
characters are always contiguous in the source. The
TextLayout.getLogicalHighlightShape
method takes two insertion
offsets and returns a Shape
that represents the highlight
region marked by the two offsets. A recommended way to show highlighting
is to fill the Shape
with the highlight color, and then draw
the TextLayout
over the Shape
.
Here is the code to get and draw the selection range:
// Retrieve highlight region for selection range.
Shape highlight =
textLayout.getLogicalHighlightShape(
anchorEnd, activeEnd);
// Fill the highlight region with the highlight color.
graphics2D.setColor(HIGHLIGHT_COLOR);
graphics2D.fill(highlight);
Here is the complete SelectionSample.java
source code.
Moving the Caret
In bidirectional text, the cursor should move smoothly through
the text on the display in the direction that corresponds to
the direction of the Arrow key being pressed. The problem, is
that right-to-left text
is positioned in the source text in the direction it is spoken
and not in the direction it is displayed. For a caret to
have a smooth journey across the display, the character offset
does not move smoothly through the source text. This point is
illustrated by the figure.
Progressing through the three screen positions shown on the bottom
of the figure from left-to-right corresponds to progressing through
the character offsets in the source text in the order of 7, 19, and 18.
The TextLayout
object handles the details for you. All
your code needs to do is update the insertion index in response to
an arrow key press:
private class ArrowKeyListener extends KeyAdapter {
/**
* Update the insertion index in response to an arrow key.
*/
private void handleArrowKey(boolean rightArrow) {
TextHitInfo newPosition;
if (rightArrow) {
newPosition =
textLayout.getNextRightHit(insertionIndex);
}
else {
newPosition =
textLayout.getNextLeftHit(insertionIndex);
}
// getNextRightHit() / getNextLeftHit() will
// return null if there is not a caret position
// to the right (left) of the current position.
if (newPosition != null) {
// Update insertionIndex.
insertionIndex = newPosition.getInsertionIndex();
// Repaint the Component so the new caret(s)
// will be displayed.
repaint();
}
}
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_LEFT ||
keyCode == KeyEvent.VK_RIGHT) {
handleArrowKey(keyCode == KeyEvent.VK_RIGHT);
}
}
}
Here is the complete
ArrowKeySample.java
source code.
Multiline Text
As you learned in the Draw Multiple
Lines of Text section of Lesson 2,
a LineBreakMeasurer
breaks a paragraph
of styled text into lines to fit into the display area.
The LineBreakMeasurer
object encapsulates enough information about bidirectional
text to produce a correct TextLayout
without any additional
code on your part.
Here is the complete
LineBreakSample.java source code.