1. /*
  2. * @(#)TextLayout.java 1.70 01/11/29
  3. *
  4. * Copyright 2002 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. /*
  8. * (C) Copyright Taligent, Inc. 1996 - 1997, All Rights Reserved
  9. * (C) Copyright IBM Corp. 1996 - 1998, All Rights Reserved
  10. *
  11. * The original version of this source code and documentation is
  12. * copyrighted and owned by Taligent, Inc., a wholly-owned subsidiary
  13. * of IBM. These materials are provided under terms of a License
  14. * Agreement between Taligent and Sun. This technology is protected
  15. * by multiple US and International patents.
  16. *
  17. * This notice and attribution to Taligent may not be removed.
  18. * Taligent is a registered trademark of Taligent, Inc.
  19. *
  20. */
  21. package java.awt.font;
  22. import java.awt.Color;
  23. import java.awt.Font;
  24. import java.awt.Graphics2D;
  25. import java.awt.Shape;
  26. import java.awt.geom.AffineTransform;
  27. import java.awt.geom.GeneralPath;
  28. import java.awt.geom.Point2D;
  29. import java.awt.geom.Rectangle2D;
  30. import java.text.AttributedString;
  31. import java.text.AttributedCharacterIterator;
  32. import java.util.Map;
  33. import java.util.Hashtable;
  34. import sun.awt.font.NativeFontWrapper;
  35. import sun.java2d.SunGraphicsEnvironment;
  36. /**
  37. *
  38. * <code>TextLayout</code> is an immutable graphical representation of styled
  39. * character data.
  40. * <p>
  41. * It provides the following capabilities:
  42. * <ul>
  43. * <li>implicit bidirectional analysis and reordering,
  44. * <li>cursor positioning and movement, including split cursors for
  45. * mixed directional text,
  46. * <li>highlighting, including both logical and visual highlighting
  47. * for mixed directional text,
  48. * <li>multiple baselines (roman, hanging, and centered),
  49. * <li>hit testing,
  50. * <li>justification,
  51. * <li>default font substitution,
  52. * <li>metric information such as ascent, descent, and advance, and
  53. * <li>rendering
  54. * </ul>
  55. * <p>
  56. * A <code>TextLayout</code> object can be rendered using
  57. * its <code>draw</code> method.
  58. * <p>
  59. * <code>TextLayout</code> can be constructed either directly or through
  60. * the use of a {@link LineBreakMeasurer}. When constructed directly, the
  61. * source text represents a single paragraph. <code>LineBreakMeasurer</code>
  62. * allows styled text to be broken into lines that fit within a particular
  63. * width. See the <code>LineBreakMeasurer</code> documentation for more
  64. * information.
  65. * <p>
  66. * <code>TextLayout</code> construction logically proceeds as follows:
  67. * <ul>
  68. * <li>paragraph attributes are extracted and examined,
  69. * <li>text is analyzed for bidirectional reordering, and reordering
  70. * information is computed if needed,
  71. * <li>text is segmented into style runs
  72. * <li>fonts are chosen for style runs, first by using a font if the
  73. * attribute {@link TextAttribute#FONT} is present, otherwise by computing
  74. * a default font using the attributes that have been defined
  75. * <li>if text is on multiple baselines, the runs or subruns are further
  76. * broken into subruns sharing a common baseline,
  77. * <li>glyphvectors are generated for each run using the chosen font,
  78. * <li>final bidirectional reordering is performed on the glyphvectors
  79. * </ul>
  80. * <p>
  81. * All graphical information returned from a <code>TextLayout</code>
  82. * object's methods is relative to the origin of the
  83. * <code>TextLayout</code>, which is the intersection of the
  84. * <code>TextLayout</code> object's baseline with its left edge. Also,
  85. * coordinates passed into a <code>TextLayout</code> object's methods
  86. * are assumed to be relative to the <code>TextLayout</code> object's
  87. * origin. Clients usually need to translate between a
  88. * <code>TextLayout</code> object's coordinate system and the coordinate
  89. * system in another object (such as a
  90. * {@link java.awt.Graphics Graphics} object).
  91. * <p>
  92. * <code>TextLayout</code> objects are constructed from styled text,
  93. * but they do not retain a reference to their source text. Thus,
  94. * changes in the text previously used to generate a <code>TextLayout</code>
  95. * do not affect the <code>TextLayout</code>.
  96. * <p>
  97. * Three methods on a <code>TextLayout</code> object
  98. * (<code>getNextRightHit</code>, <code>getNextLeftHit</code>, and
  99. * <code>hitTestChar</code>) return instances of {@link TextHitInfo}.
  100. * The offsets contained in these <code>TextHitInfo</code> objects
  101. * are relative to the start of the <code>TextLayout</code>, <b>not</b>
  102. * to the text used to create the <code>TextLayout</code>. Similarly,
  103. * <code>TextLayout</code> methods that accept <code>TextHitInfo</code>
  104. * instances as parameters expect the <code>TextHitInfo</code> object's
  105. * offsets to be relative to the <code>TextLayout</code>, not to any
  106. * underlying text storage model.
  107. * <p>
  108. * <strong>Examples</strong>:<p>
  109. * Constructing and drawing a <code>TextLayout</code> and its bounding
  110. * rectangle:
  111. * <blockquote><pre>
  112. * Graphics2D g = ...;
  113. * Point2D loc = ...;
  114. * Font font = Font.getFont("Helvetica-bold-italic");
  115. * FontRenderContext frc = g.getFontRenderContext();
  116. * TextLayout layout = new TextLayout("This is a string", font, frc);
  117. * layout.draw(g, loc.getX(), loc.getY());
  118. *
  119. * Rectangle2D bounds = layout.getBounds();
  120. * bounds.setRect(bounds.getX()+loc.getX(),
  121. * bounds.getY()+loc.getY(),
  122. * bounds.getWidth(),
  123. * bounds.getHeight())
  124. * g.draw(bounds);
  125. * </pre>
  126. * </blockquote>
  127. * <p>
  128. * Hit-testing a <code>TextLayout</code> (determining which character is at
  129. * a particular graphical location):
  130. * <blockquote><pre>
  131. * Point2D click = ...;
  132. * TextHitInfo hit = layout.hitTestChar(
  133. * (float) (click.getX() - loc.getX()),
  134. * (float) (click.getY() - loc.getY()));
  135. * </pre>
  136. * </blockquote>
  137. * <p>
  138. * Responding to a right-arrow key press:
  139. * <blockquoute><pre>
  140. * int insertionIndex = ...;
  141. * TextHitInfo next = layout.getNextRightHit(insertionIndex);
  142. * if (next != null) {
  143. * // translate graphics to origin of layout on screen
  144. * g.translate(loc.getX(), loc.getY());
  145. * Shape[] carets = layout.getCaretShapes(next.getInsertionIndex());
  146. * g.draw(carets[0]);
  147. * if (carets[1] != null) {
  148. * g.draw(carets[1]);
  149. * }
  150. * }
  151. * </pre></blockquote>
  152. * <p>
  153. * Drawing a selection range corresponding to a substring in the source text.
  154. * The selected area may not be visually contiguous:
  155. * <blockquoute><pre>
  156. * // selStart, selLimit should be relative to the layout,
  157. * // not to the source text
  158. *
  159. * int selStart = ..., selLimit = ...;
  160. * Color selectionColor = ...;
  161. * Shape selection = layout.getLogicalHighlightShape(selStart, selLimit);
  162. * // selection may consist of disjoint areas
  163. * // graphics is assumed to be tranlated to origin of layout
  164. * g.setColor(selectionColor);
  165. * g.fill(selection);
  166. * </pre></blockquote>
  167. * <p>
  168. * Drawing a visually contiguous selection range. The selection range may
  169. * correspond to more than one substring in the source text. The ranges of
  170. * the corresponding source text substrings can be obtained with
  171. * <code>getLogicalRangesForVisualSelection()</code>:
  172. * <blockquoute><pre>
  173. * TextHitInfo selStart = ..., selLimit = ...;
  174. * Shape selection = layout.getVisualHighlightShape(selStart, selLimit);
  175. * g.setColor(selectionColor);
  176. * g.fill(selection);
  177. * int[] ranges = getLogicalRangesForVisualSelection(selStart, selLimit);
  178. * // ranges[0], ranges[1] is the first selection range,
  179. * // ranges[2], ranges[3] is the second selection range, etc.
  180. * </pre></blockquote>
  181. * <p>
  182. * @see LineBreakMeasurer
  183. * @see TextAttribute
  184. * @see TextHitInfo
  185. */
  186. public final class TextLayout implements Cloneable {
  187. private int characterCount;
  188. private boolean isVerticalLine = false;
  189. private byte baseline;
  190. private float[] baselineOffsets; // why have these ?
  191. private TextLine textLine;
  192. // cached values computed from GlyphSets and set info:
  193. // all are recomputed from scratch in buildCache()
  194. private TextLine.TextLineMetrics lineMetrics = null;
  195. private float visibleAdvance;
  196. private int hashCodeCache;
  197. /*
  198. * TextLayouts are supposedly immutable. If you mutate a TextLayout under
  199. * the covers (like the justification code does) you'll need to set this
  200. * back to false. Could be replaced with textLine != null <--> cacheIsValid.
  201. */
  202. private boolean cacheIsValid = false;
  203. // This value is obtained from an attribute, and constrained to the
  204. // interval [0,1]. If 0, the layout cannot be justified.
  205. private float justifyRatio;
  206. // If a layout is produced by justification, then that layout
  207. // cannot be justified. To enforce this constraint the
  208. // justifyRatio of the justified layout is set to this value.
  209. private static final float ALREADY_JUSTIFIED = -53.9f;
  210. // dx and dy specify the distance between the TextLayout's origin
  211. // and the origin of the leftmost GlyphSet (TextLayoutComponent,
  212. // actually). They were used for hanging punctuation support,
  213. // which is no longer implemented. Currently they are both always 0,
  214. // and TextLayout is not guaranteed to work with non-zero dx, dy
  215. // values right now. They were left in as an aide and reminder to
  216. // anyone who implements hanging punctuation or other similar stuff.
  217. // They are static now so they don't take up space in TextLayout
  218. // instances.
  219. private static float dx;
  220. private static float dy;
  221. /*
  222. * Natural bounds is used internally. It is built on demand in
  223. * getNaturalBounds.
  224. */
  225. private Rectangle2D naturalBounds = null;
  226. /*
  227. * boundsRect encloses all of the bits this TextLayout can draw. It
  228. * is build on demand in getBounds.
  229. */
  230. private Rectangle2D boundsRect = null;
  231. /*
  232. * flag to supress/allow carets inside of ligatures when hit testing or
  233. * arrow-keying
  234. */
  235. private boolean caretsInLigaturesAreAllowed = false;
  236. /**
  237. * Defines a policy for determining the strong caret location.
  238. * This class contains one method, <code>getStrongCaret</code>, which
  239. * is used to specify the policy that determines the strong caret in
  240. * dual-caret text. The strong caret is used to move the caret to the
  241. * left or right. Instances of this class can be passed to
  242. * <code>getCaretShapes</code>, <code>getNextLeftHit</code> and
  243. * <code>getNextRightHit</code> to customize strong caret
  244. * selection.
  245. * <p>
  246. * To specify alternate caret policies, subclass <code>CaretPolicy</code>
  247. * and override <code>getStrongCaret</code>. <code>getStrongCaret</code>
  248. * should inspect the two <code>TextHitInfo</code> arguments and choose
  249. * one of them as the strong caret.
  250. * <p>
  251. * Most clients do not need to use this class.
  252. */
  253. public static class CaretPolicy {
  254. /**
  255. * Constructs a <code>CaretPolicy</code>.
  256. */
  257. public CaretPolicy() {
  258. }
  259. /**
  260. * Chooses one of the specified <code>TextHitInfo</code> instances as
  261. * a strong caret in the specified <code>TextLayout</code>.
  262. * @param hit1 a valid hit in <code>layout</code>
  263. * @param hit2 a valid hit in <code>layout</code>
  264. * @param layout the <code>TextLayout</code> in which
  265. * <code>hit1</code> and <code>hit2</code> are used
  266. * @return <code>hit1</code> or <code>hit2</code>
  267. * (or an equivalent <code>TextHitInfo</code>), indicating the
  268. * strong caret.
  269. */
  270. public TextHitInfo getStrongCaret(TextHitInfo hit1,
  271. TextHitInfo hit2,
  272. TextLayout layout) {
  273. // default implmentation just calls private method on layout
  274. return layout.getStrongHit(hit1, hit2);
  275. }
  276. }
  277. /**
  278. * This <code>CaretPolicy</code> is used when a policy is not specified
  279. * by the client. With this policy, a hit on a character whose direction
  280. * is the same as the line direction is stronger than a hit on a
  281. * counterdirectional character. If the characters' directions are
  282. * the same, a hit on the leading edge of a character is stronger
  283. * than a hit on the trailing edge of a character.
  284. */
  285. public static final CaretPolicy DEFAULT_CARET_POLICY = new CaretPolicy();
  286. /**
  287. * Constructs a <code>TextLayout</code> from a <code>String</code>
  288. * and a {@link Font}. All the text is styled using the specified
  289. * <code>Font</code>.
  290. * <p>
  291. * The <code>String</code> must specify a single paragraph of text,
  292. * because an entire paragraph is required for the bidirectional
  293. * algorithm.
  294. * @param str the text to display
  295. * @param font a <code>Font</code> used to style the text
  296. * @param frc contains the information needed to correctly measure the
  297. * text
  298. */
  299. public TextLayout(String string, Font font, FontRenderContext frc) {
  300. if (font == null) {
  301. throw new IllegalArgumentException("Null font passed to TextLayout constructor.");
  302. }
  303. if (string == null) {
  304. throw new IllegalArgumentException("Null string passed to TextLayout constructor.");
  305. }
  306. if (string.length() == 0) {
  307. throw new IllegalArgumentException("Zero length string passed to TextLayout constructor.");
  308. }
  309. char[] text = string.toCharArray();
  310. if (sameBaselineUpTo(font, text, 0, text.length) == text.length) {
  311. fastInit(text, 0, text.length, font, null, frc);
  312. } else {
  313. AttributedString as = new AttributedString(string);
  314. as.addAttribute(TextAttribute.FONT, font);
  315. standardInit(as.getIterator(), text, frc);
  316. }
  317. }
  318. /**
  319. * Constructs a <code>TextLayout</code> from a <code>String</code>
  320. * and an attribute set.
  321. * <p>
  322. * All the text is styled using the provided attributes.
  323. * <p>
  324. * <code>string</code> must specify a single paragraph of text because an
  325. * entire paragraph is required for the bidirectional algorithm.
  326. * @param str the text to display
  327. * @param attributes the attributes used to style the text
  328. * @param frc contains the information needed to correctly measure the
  329. * text
  330. */
  331. public TextLayout(String string, Map attributes, FontRenderContext frc) {
  332. if (string == null) {
  333. throw new IllegalArgumentException("Null string passed to TextLayout constructor.");
  334. }
  335. if (attributes == null) {
  336. throw new IllegalArgumentException("Null map passed to TextLayout constructor.");
  337. }
  338. if (string.length() == 0) {
  339. throw new IllegalArgumentException("Zero length string passed to TextLayout constructor.");
  340. }
  341. char[] text = string.toCharArray();
  342. Font font = singleFont(text, 0, text.length, attributes);
  343. if (font != null) {
  344. fastInit(text, 0, text.length, font, attributes, frc);
  345. } else {
  346. AttributedString as = new AttributedString(string, attributes);
  347. standardInit(as.getIterator(), text, frc);
  348. }
  349. }
  350. /*
  351. * Determines a font for the attributes, and if a single font can render
  352. * all the text on one baseline, return it, otherwise null. If the
  353. * attributes specify a font, assume it can display all the text without
  354. * checking.
  355. * If the AttributeSet contains an embedded graphic, return null.
  356. */
  357. private static Font singleFont(char[] text,
  358. int start,
  359. int limit,
  360. Map attributes) {
  361. if (attributes.get(TextAttribute.CHAR_REPLACEMENT) != null) {
  362. return null;
  363. }
  364. Font font = (Font)attributes.get(TextAttribute.FONT);
  365. if (font == null) {
  366. font = Font.getFont(attributes); // uses static cache in Font;
  367. if (font.canDisplayUpTo(text, start, limit) != limit) {
  368. return null;
  369. }
  370. }
  371. if (sameBaselineUpTo(font, text, start, limit) != limit) {
  372. return null;
  373. }
  374. return font;
  375. }
  376. /**
  377. * Constructs a <code>TextLayout</code> from an iterator over styled text.
  378. * <p>
  379. * The iterator must specify a single paragraph of text because an
  380. * entire paragraph is required for the bidirectional
  381. * algorithm.
  382. * @param text the styled text to display
  383. * @param frc contains the information needed to correctly measure the
  384. * text
  385. */
  386. public TextLayout(AttributedCharacterIterator text, FontRenderContext frc) {
  387. if (text == null) {
  388. throw new IllegalArgumentException("Null iterator passed to TextLayout constructor.");
  389. }
  390. int start = text.getBeginIndex();
  391. int limit = text.getEndIndex();
  392. if (start == limit) {
  393. throw new IllegalArgumentException("Zero length iterator passed to TextLayout constructor.");
  394. }
  395. int len = limit - start;
  396. text.first();
  397. char[] chars = new char[len];
  398. int n = 0;
  399. for (char c = text.first(); c != text.DONE; c = text.next()) {
  400. chars[n++] = c;
  401. }
  402. text.first();
  403. if (text.getRunLimit() == limit) {
  404. text.first();
  405. Map attributes = text.getAttributes();
  406. Font font = singleFont(chars, 0, len, attributes);
  407. if (font != null) {
  408. fastInit(chars, 0, len, font, attributes, frc);
  409. return;
  410. }
  411. }
  412. standardInit(text, chars, frc);
  413. }
  414. /**
  415. * Creates a <code>TextLayout</code> from a {@link TextLine} and
  416. * some paragraph data. This method is used by {@link TextMeasurer}.
  417. * @param textLine the line measurement attributes to apply to the
  418. * the resulting <code>TextLayout</code>
  419. * @param baseline the baseline of the text
  420. * @param baselineOffsets the baseline offsets for this
  421. * <code>TextLayout</code>. This should already be normalized to
  422. * <code>baseline</code>
  423. * @param justifyRatio <code>0</code> if the <code>TextLayout</code>
  424. * cannot be justified; <code>1</code> otherwise.
  425. */
  426. TextLayout(TextLine textLine,
  427. byte baseline,
  428. float[] baselineOffsets,
  429. float justifyRatio) {
  430. this.characterCount = textLine.characterCount();
  431. this.baseline = baseline;
  432. this.baselineOffsets = baselineOffsets;
  433. this.textLine = textLine;
  434. this.justifyRatio = justifyRatio;
  435. }
  436. /**
  437. * Initialize the paragraph-specific data.
  438. */
  439. private void paragraphInit(byte aBaseline, LineMetrics lm, Map paragraphAttrs) {
  440. // Object vf = paragraphAttrs.get(TextAttribute.ORIENTATION);
  441. // isVerticalLine = TextAttribute.ORIENTATION_VERTICAL.equals(vf);
  442. isVerticalLine = false;
  443. baseline = aBaseline;
  444. baselineOffsets = lm.getBaselineOffsets();
  445. // normalize to current baseline
  446. if (baselineOffsets[baseline] != 0) {
  447. float base = baselineOffsets[baseline];
  448. float[] temp = new float[baselineOffsets.length];
  449. for (int i = 0; i < temp.length; i++)
  450. temp[i] = baselineOffsets[i] - base;
  451. baselineOffsets = temp;
  452. }
  453. /*
  454. * if justification is defined, use it (forcing to valid range)
  455. * otherwise set to 1.0
  456. */
  457. Float justifyLF = (Float) paragraphAttrs.get(TextAttribute.JUSTIFICATION);
  458. if (justifyLF == null) {
  459. justifyRatio = 1;
  460. } else {
  461. justifyRatio = justifyLF.floatValue();
  462. if (justifyRatio < 0) {
  463. justifyRatio = 0;
  464. } else if (justifyRatio > 1) {
  465. justifyRatio = 1;
  466. }
  467. }
  468. }
  469. /*
  470. * the fast init generates a single glyph set. This requires:
  471. * all one style
  472. * all renderable by one font (ie no embedded graphics)
  473. * all on one baseline
  474. */
  475. private void fastInit(char[] text, int start, int limit, Font font, Map attrs, FontRenderContext frc) {
  476. // Object vf = attrs.get(TextAttribute.ORIENTATION);
  477. // isVerticalLine = TextAttribute.ORIENTATION_VERTICAL.equals(vf);
  478. isVerticalLine = false;
  479. LineMetrics lm = font.getLineMetrics(text, start, start+1, frc);
  480. byte glyphBaseline = (byte) lm.getBaselineIndex();
  481. if (attrs == null) {
  482. baseline = glyphBaseline;
  483. baselineOffsets = lm.getBaselineOffsets();
  484. justifyRatio = 1.0f;
  485. } else {
  486. paragraphInit(glyphBaseline, lm, attrs);
  487. }
  488. characterCount = limit-start;
  489. textLine = TextLine.fastCreateTextLine(frc, text, start, limit, font, lm, attrs);
  490. }
  491. /*
  492. * the standard init generates multiple glyph sets based on style,
  493. * renderable, and baseline runs.
  494. * @param chars the text in the iterator, extracted into a char array
  495. */
  496. private void standardInit(AttributedCharacterIterator text, char[] chars, FontRenderContext frc) {
  497. characterCount = text.getEndIndex() - text.getBeginIndex();
  498. // set paragraph attributes
  499. {
  500. // If there's an embedded graphic at the start of the
  501. // paragraph, look for the first non-graphic character
  502. // and use it and its font to initialize the paragraph.
  503. // If not, use the first graphic to initialize.
  504. char firstChar = text.first();
  505. Map paragraphAttrs = text.getAttributes();
  506. Map fontAttrs = paragraphAttrs;
  507. GraphicAttribute firstGraphic = (GraphicAttribute)
  508. paragraphAttrs.get(TextAttribute.CHAR_REPLACEMENT);
  509. boolean useFirstGraphic = false;
  510. if (firstGraphic != null) {
  511. useFirstGraphic = true;
  512. for (firstChar = text.setIndex(text.getRunLimit());
  513. firstChar != text.DONE;
  514. firstChar = text.setIndex(text.getRunLimit())) {
  515. fontAttrs = text.getAttributes();
  516. if (fontAttrs.get(TextAttribute.CHAR_REPLACEMENT) == null) {
  517. useFirstGraphic = false;
  518. break;
  519. }
  520. }
  521. if (useFirstGraphic) {
  522. firstChar = text.first();
  523. }
  524. }
  525. if (useFirstGraphic) {
  526. // hmmm what to do here? Just try to supply reasonable
  527. // values I guess.
  528. byte defaultBaseline = getBaselineFromGraphic(firstGraphic);
  529. Font dummyFont = new Font(new Hashtable(5, (float)0.9));
  530. LineMetrics lm = dummyFont.getLineMetrics(chars, 0, 1, frc);
  531. float[] defaultOffsets = lm.getBaselineOffsets();
  532. paragraphInit(defaultBaseline, lm, paragraphAttrs);
  533. }
  534. else {
  535. Font defaultFont = (Font)fontAttrs.get(TextAttribute.FONT);
  536. if (defaultFont == null) {
  537. defaultFont = SunGraphicsEnvironment.getBestFontFor(
  538. text, text.getIndex(), text.getEndIndex());
  539. }
  540. int charsStart = text.getIndex() - text.getBeginIndex();
  541. LineMetrics lm = defaultFont.getLineMetrics(chars, charsStart, charsStart+1, frc);
  542. paragraphInit(defaultFont.getBaselineFor(firstChar), lm, paragraphAttrs);
  543. }
  544. }
  545. textLine = TextLine.standardCreateTextLine(frc, text, chars, baselineOffsets);
  546. }
  547. /*
  548. * A utility to rebuild the ascent/descent/leading/advance cache.
  549. * You'll need to call this if you clone and mutate (like justification,
  550. * editing methods do)
  551. */
  552. private void ensureCache() {
  553. if (!cacheIsValid) {
  554. buildCache();
  555. }
  556. }
  557. private void buildCache() {
  558. lineMetrics = textLine.getMetrics();
  559. // compute visibleAdvance
  560. if (textLine.isDirectionLTR()) {
  561. int lastNonSpace = characterCount-1;
  562. while (lastNonSpace != -1) {
  563. int logIndex = textLine.visualToLogical(lastNonSpace);
  564. if (!textLine.isCharSpace(logIndex)) {
  565. break;
  566. }
  567. else {
  568. --lastNonSpace;
  569. }
  570. }
  571. if (lastNonSpace == characterCount-1) {
  572. visibleAdvance = lineMetrics.advance;
  573. }
  574. else if (lastNonSpace == -1) {
  575. visibleAdvance = 0;
  576. }
  577. else {
  578. int logIndex = textLine.visualToLogical(lastNonSpace);
  579. visibleAdvance = textLine.getCharLinePosition(logIndex)
  580. + textLine.getCharAdvance(logIndex);
  581. }
  582. }
  583. else {
  584. int leftmostNonSpace = 0;
  585. while (leftmostNonSpace != characterCount) {
  586. int logIndex = textLine.visualToLogical(leftmostNonSpace);
  587. if (!textLine.isCharSpace(logIndex)) {
  588. break;
  589. }
  590. else {
  591. ++leftmostNonSpace;
  592. }
  593. }
  594. if (leftmostNonSpace == characterCount) {
  595. visibleAdvance = 0;
  596. }
  597. else if (leftmostNonSpace == 0) {
  598. visibleAdvance = lineMetrics.advance;
  599. }
  600. else {
  601. int logIndex = textLine.visualToLogical(leftmostNonSpace);
  602. float pos = textLine.getCharLinePosition(logIndex);
  603. visibleAdvance = lineMetrics.advance - pos;
  604. }
  605. }
  606. // naturalBounds, boundsRect will be generated on demand
  607. naturalBounds = null;
  608. boundsRect = null;
  609. // hashCode will be regenerated on demand
  610. hashCodeCache = 0;
  611. cacheIsValid = true;
  612. }
  613. private Rectangle2D getNaturalBounds() {
  614. ensureCache();
  615. if (naturalBounds == null) {
  616. int leftmostCharIndex = textLine.visualToLogical(0);
  617. float angle = textLine.getCharAngle(leftmostCharIndex);
  618. float leftOrTop = isVerticalLine? -dy : -dx;
  619. if (angle < 0) {
  620. leftOrTop += angle*textLine.getCharAscent(leftmostCharIndex);
  621. }
  622. else if (angle > 0) {
  623. leftOrTop -= angle*textLine.getCharDescent(leftmostCharIndex);
  624. }
  625. int rightmostCharIndex = textLine.visualToLogical(characterCount-1);
  626. angle = textLine.getCharAngle(rightmostCharIndex);
  627. float rightOrBottom = lineMetrics.advance;
  628. if (angle < 0) {
  629. rightOrBottom -=
  630. angle*textLine.getCharDescent(rightmostCharIndex);
  631. }
  632. else if (angle > 0) {
  633. rightOrBottom +=
  634. angle*textLine.getCharAscent(rightmostCharIndex);
  635. }
  636. float lineDim = rightOrBottom - leftOrTop;
  637. if (isVerticalLine) {
  638. naturalBounds = new Rectangle2D.Float(
  639. -lineMetrics.descent, leftOrTop,
  640. lineMetrics.ascent + lineMetrics.descent, lineDim);
  641. }
  642. else {
  643. naturalBounds = new Rectangle2D.Float(
  644. leftOrTop, -lineMetrics.ascent,
  645. lineDim, lineMetrics.ascent + lineMetrics.descent);
  646. }
  647. }
  648. return naturalBounds;
  649. }
  650. /**
  651. * Creates a copy of this <code>TextLayout</code>.
  652. */
  653. protected Object clone() {
  654. /*
  655. * !!! I think this is safe. Once created, nothing mutates the
  656. * glyphvectors or arrays. But we need to make sure.
  657. * {jbr} actually, that's not quite true. The justification code
  658. * mutates after cloning. It doesn't actually change the glyphvectors
  659. * (that's impossible) but it replaces them with justified sets. This
  660. * is a problem for GlyphIterator creation, since new GlyphIterators
  661. * are created by cloning a prototype. If the prototype has outdated
  662. * glyphvectors, so will the new ones. A partial solution is to set the
  663. * prototypical GlyphIterator to null when the glyphvectors change. If
  664. * you forget this one time, you're hosed.
  665. */
  666. try {
  667. return super.clone();
  668. }
  669. catch (CloneNotSupportedException e) {
  670. throw new InternalError();
  671. }
  672. }
  673. /*
  674. * Utility to throw an expection if an invalid TextHitInfo is passed
  675. * as a parameter. Avoids code duplication.
  676. */
  677. private void checkTextHit(TextHitInfo hit) {
  678. if (hit == null) {
  679. throw new IllegalArgumentException("TextHitInfo is null.");
  680. }
  681. if (hit.getInsertionIndex() < 0 ||
  682. hit.getInsertionIndex() > characterCount) {
  683. throw new IllegalArgumentException("TextHitInfo is out of range");
  684. }
  685. }
  686. /**
  687. * Creates a copy of this <code>TextLayout</code> justified to the
  688. * specified width.
  689. * <p>
  690. * If this <code>TextLayout</code> has already been justified, an
  691. * exception is thrown. If this <code>TextLayout</code> object's
  692. * justification ratio is zero, a <code>TextLayout</code> identical
  693. * to this <code>TextLayout</code> is returned.
  694. * @param justificationWidth the width to use when justifying the line.
  695. * For best results, it should not be too different from the current
  696. * advance of the line.
  697. * @return a <code>TextLayout</code> justified to the specified width.
  698. * @exception Error if this layout has already been justified, an Error is
  699. * thrown.
  700. */
  701. public TextLayout getJustifiedLayout(float justificationWidth) {
  702. if (justificationWidth <= 0) {
  703. throw new IllegalArgumentException("justificationWidth <= 0 passed to TextLayout.getJustifiedLayout()");
  704. }
  705. if (justifyRatio == ALREADY_JUSTIFIED) {
  706. throw new Error("Can't justify again.");
  707. }
  708. // default justification range to exclude trailing logical whitespace
  709. int limit = characterCount;
  710. while (limit > 0 && textLine.isCharWhitespace(limit-1)) {
  711. --limit;
  712. }
  713. TextLine newLine = textLine.getJustifiedLine(justificationWidth, justifyRatio, 0, limit);
  714. if (newLine != null) {
  715. return new TextLayout(newLine, baseline, baselineOffsets, ALREADY_JUSTIFIED);
  716. }
  717. return this;
  718. }
  719. /**
  720. * Justify this layout. Overridden by subclassers to control justification
  721. * (if there were subclassers, that is...)
  722. *
  723. * The layout will only justify if the paragraph attributes (from the
  724. * source text, possibly defaulted by the layout attributes) indicate a
  725. * non-zero justification ratio. The text will be justified to the
  726. * indicated width. The current implementation also adjusts hanging
  727. * punctuation and trailing whitespace to overhang the justification width.
  728. * Once justified, the layout may not be rejustified.
  729. * <p>
  730. * Some code may rely on immutablity of layouts. Subclassers should not
  731. * call this directly, but instead should call getJustifiedLayout, which
  732. * will call this method on a clone of this layout, preserving
  733. * the original.
  734. *
  735. * @param justificationWidth the width to use when justifying the line.
  736. * For best results, it should not be too different from the current
  737. * advance of the line.
  738. * @see #getJustifiedLayout(float)
  739. */
  740. protected void handleJustify(float justificationWidth) {
  741. // never called
  742. }
  743. /**
  744. * Returns the baseline for this <code>TextLayout</code>.
  745. * The baseline is one of the values defined in <code>Font</code>,
  746. * which are roman, centered and hanging. Ascent and descent are
  747. * relative to this baseline. The <code>baselineOffsets</code>
  748. * are also relative to this baseline.
  749. * @return the baseline of this <code>TextLayout</code>.
  750. * @see #getBaselineOffsets()
  751. * @see Font
  752. */
  753. public byte getBaseline() {
  754. return baseline;
  755. }
  756. /**
  757. * Returns the offsets array for the baselines used for this
  758. * <code>TextLayout</code>.
  759. * <p>
  760. * The array is indexed by one of the values defined in
  761. * <code>Font</code>, which are roman, centered and hanging. The
  762. * values are relative to this <code>TextLayout</code> object's
  763. * baseline, so that <code>getBaselineOffsets[getBaseline()] == 0</code>.
  764. * Offsets are added to the position of the <code>TextLayout</code>
  765. * object's baseline to get the position for the new baseline.
  766. * @return the offsets array containing the baselines used for this
  767. * <code>TextLayout</code>.
  768. * @see #getBaseline()
  769. * @see Font
  770. */
  771. public float[] getBaselineOffsets() {
  772. float[] offsets = new float[baselineOffsets.length];
  773. System.arraycopy(baselineOffsets, 0, offsets, 0, offsets.length);
  774. return offsets;
  775. }
  776. /**
  777. * Returns the advance of this <code>TextLayout</code>.
  778. * The advance is the distance from the origin to the advance of the
  779. * rightmost (bottommost) character measuring in the line direction.
  780. * @return the advance of this <code>TextLayout</code>.
  781. */
  782. public float getAdvance() {
  783. ensureCache();
  784. return lineMetrics.advance;
  785. }
  786. /**
  787. * Returns the advance of this <code>TextLayout</code>, minus trailing
  788. * whitespace.
  789. * @return the advance of this <code>TextLayout</code> without the
  790. * trailing whitespace.
  791. * @see #getAdvance()
  792. */
  793. public float getVisibleAdvance() {
  794. ensureCache();
  795. return visibleAdvance;
  796. }
  797. /**
  798. * Returns the ascent of this <code>TextLayout</code>.
  799. * The ascent is the distance from the top (right) of the
  800. * <code>TextLayout</code> to the baseline. It is always either
  801. * positive or zero. The ascent is sufficient to
  802. * accomodate superscripted text and is the maximum of the sum of the
  803. * ascent, offset, and baseline of each glyph.
  804. * @return the ascent of this <code>TextLayout</code>.
  805. */
  806. public float getAscent() {
  807. ensureCache();
  808. return lineMetrics.ascent;
  809. }
  810. /**
  811. * Returns the descent of this <code>TextLayout</code>.
  812. * The descent is the distance from the baseline to the bottom (left) of
  813. * the <code>TextLayout</code>. It is always either positive or zero.
  814. * The descent is sufficient to accomodate subscripted text and is the
  815. * maximum of the sum of the descent, offset, and baseline of each glyph.
  816. * @return the descent of this <code>TextLayout</code>.
  817. */
  818. public float getDescent() {
  819. ensureCache();
  820. return lineMetrics.descent;
  821. }
  822. /**
  823. * Returns the leading of the <code>TextLayout</code>.
  824. * The leading is the suggested interline spacing for this
  825. * <code>TextLayout</code>.
  826. * <p>
  827. * The leading is computed from the leading, descent, and baseline
  828. * of all glyphvectors in the <code>TextLayout</code>. The algorithm
  829. * is roughly as follows:
  830. * <blockquote><pre>
  831. * maxD = 0;
  832. * maxDL = 0;
  833. * for (GlyphVector g in all glyphvectors) {
  834. * maxD = max(maxD, g.getDescent() + offsets[g.getBaseline()]);
  835. * maxDL = max(maxDL, g.getDescent() + g.getLeading() +
  836. * offsets[g.getBaseline()]);
  837. * }
  838. * return maxDL - maxD;
  839. * </pre></blockquote>
  840. * @return the leading of this <code>TextLayout</code>.
  841. */
  842. public float getLeading() {
  843. ensureCache();
  844. return lineMetrics.leading;
  845. }
  846. /**
  847. * Returns the bounds of this <code>TextLayout</code>.
  848. * The bounds contains all of the pixels the <code>TextLayout</code>
  849. * can draw. It might not coincide exactly with the ascent, descent,
  850. * origin or advance of the <code>TextLayout</code>.
  851. * @return a {@link Rectangle2D} that is the bounds of this
  852. * <code>TextLayout</code>.
  853. */
  854. public Rectangle2D getBounds() {
  855. ensureCache();
  856. if (boundsRect == null) {
  857. Rectangle2D lineBounds = textLine.getBounds();
  858. if (dx != 0 || dy != 0) {
  859. lineBounds.setRect(lineBounds.getX() - dx,
  860. lineBounds.getY() - dy,
  861. lineBounds.getWidth(),
  862. lineBounds.getHeight());
  863. }
  864. boundsRect = lineBounds;
  865. }
  866. Rectangle2D bounds = new Rectangle2D.Float();
  867. bounds.setRect(boundsRect);
  868. return bounds;
  869. }
  870. /**
  871. * Returns <code>true</code> if this <code>TextLayout</code> has
  872. * a left-to-right base direction or <code>false</code> if it has
  873. * a right-to-left base direction. The <code>TextLayout</code>
  874. * has a base direction of either left-to-right (LTR) or
  875. * right-to-left (RTL). The base direction is independent of the
  876. * actual direction of text on the line, which may be either LTR,
  877. * RTL, or mixed. Left-to-right layouts by default should position
  878. * flush left. If the layout is on a tabbed line, the
  879. * tabs run left to right, so that logically successive layouts position
  880. * left to right. The opposite is true for RTL layouts. By default they
  881. * should position flush left, and tabs run right-to-left.
  882. * @return <code>true</code> if the base direction of this
  883. * <code>TextLayout</code> is left-to-right; <code>false</code>
  884. * otherwise.
  885. */
  886. public boolean isLeftToRight() {
  887. return textLine.isDirectionLTR();
  888. }
  889. /**
  890. * Returns <code>true</code> if this <code>TextLayout</code> is vertical.
  891. * @return <code>true</code> if this <code>TextLayout</code> is vertical;
  892. * <code>false</code> otherwise.
  893. */
  894. public boolean isVertical() {
  895. return isVerticalLine;
  896. }
  897. /**
  898. * Returns the number of characters represented by this
  899. * <code>TextLayout</code>.
  900. * @return the number of characters in this <code>TextLayout</code>.
  901. */
  902. public int getCharacterCount() {
  903. return characterCount;
  904. }
  905. /*
  906. * carets and hit testing
  907. *
  908. * Positions on a text line are represented by instances of TextHitInfo.
  909. * Any TextHitInfo with characterOffset between 0 and characterCount-1,
  910. * inclusive, represents a valid position on the line. Additionally,
  911. * [-1, trailing] and [characterCount, leading] are valid positions, and
  912. * represent positions at the logical start and end of the line,
  913. * respectively.
  914. *
  915. * The characterOffsets in TextHitInfo's used and returned by TextLayout
  916. * are relative to the beginning of the text layout, not necessarily to
  917. * the beginning of the text storage the client is using.
  918. *
  919. *
  920. * Every valid TextHitInfo has either one or two carets associated with it.
  921. * A caret is a visual location in the TextLayout indicating where text at
  922. * the TextHitInfo will be displayed on screen. If a TextHitInfo
  923. * represents a location on a directional boundary, then there are two
  924. * possible visible positions for newly inserted text. Consider the
  925. * following example, in which capital letters indicate right-to-left text,
  926. * and the overall line direction is left-to-right:
  927. *
  928. * Text Storage: [ a, b, C, D, E, f ]
  929. * Display: a b E D C f
  930. *
  931. * The text hit info (1, t) represents the trailing side of 'b'. If 'q',
  932. * a left-to-right character is inserted into the text storage at this
  933. * location, it will be displayed between the 'b' and the 'E':
  934. *
  935. * Text Storage: [ a, b, q, C, D, E, f ]
  936. * Display: a b q E D C f
  937. *
  938. * However, if a 'W', which is right-to-left, is inserted into the storage
  939. * after 'b', the storage and display will be:
  940. *
  941. * Text Storage: [ a, b, W, C, D, E, f ]
  942. * Display: a b E D C W f
  943. *
  944. * So, for the original text storage, two carets should be displayed for
  945. * location (1, t): one visually between 'b' and 'E' and one visually
  946. * between 'C' and 'f'.
  947. *
  948. *
  949. * When two carets are displayed for a TextHitInfo, one caret is the
  950. * 'strong' caret and the other is the 'weak' caret. The strong caret
  951. * indicates where an inserted character will be displayed when that
  952. * character's direction is the same as the direction of the TextLayout.
  953. * The weak caret shows where an character inserted character will be
  954. * displayed when the character's direction is opposite that of the
  955. * TextLayout.
  956. *
  957. *
  958. * Clients should not be overly concerned with the details of correct
  959. * caret display. TextLayout.getCaretShapes(TextHitInfo) will return an
  960. * array of two paths representing where carets should be displayed.
  961. * The first path in the array is the strong caret; the second element,
  962. * if non-null, is the weak caret. If the second element is null,
  963. * then there is no weak caret for the given TextHitInfo.
  964. *
  965. *
  966. * Since text can be visually reordered, logically consecutive
  967. * TextHitInfo's may not be visually consecutive. One implication of this
  968. * is that a client cannot tell from inspecting a TextHitInfo whether the
  969. * hit represents the first (or last) caret in the layout. Clients
  970. * can call getVisualOtherHit(); if the visual companion is
  971. * (-1, TRAILING) or (characterCount, LEADING), then the hit is at the
  972. * first (last) caret position in the layout.
  973. */
  974. private float[] getCaretInfo(int caret, Rectangle2D bounds) {
  975. float top1X, top2X;
  976. float bottom1X, bottom2X;
  977. if (caret == 0 || caret == characterCount) {
  978. float pos;
  979. int logIndex;
  980. if (caret == characterCount) {
  981. logIndex = textLine.visualToLogical(characterCount-1);
  982. pos = textLine.getCharLinePosition(logIndex)
  983. + textLine.getCharAdvance(logIndex);
  984. }
  985. else {
  986. logIndex = textLine.visualToLogical(caret);
  987. pos = textLine.getCharLinePosition(logIndex);
  988. }
  989. float angle = textLine.getCharAngle(logIndex);
  990. top1X = top2X = pos + angle*textLine.getCharAscent(logIndex);
  991. bottom1X = bottom2X = pos - angle*textLine.getCharDescent(logIndex);
  992. }
  993. else {
  994. {
  995. int logIndex = textLine.visualToLogical(caret-1);
  996. float angle1 = textLine.getCharAngle(logIndex);
  997. float pos1 = textLine.getCharLinePosition(logIndex)
  998. + textLine.getCharAdvance(logIndex);
  999. if (angle1 != 0) {
  1000. top1X = pos1 + angle1*textLine.getCharAscent(logIndex);
  1001. bottom1X = pos1 - angle1*textLine.getCharDescent(logIndex);
  1002. }
  1003. else {
  1004. top1X = bottom1X = pos1;
  1005. }
  1006. }
  1007. {
  1008. int logIndex = textLine.visualToLogical(caret);
  1009. float angle2 = textLine.getCharAngle(logIndex);
  1010. float pos2 = textLine.getCharLinePosition(logIndex);
  1011. if (angle2 != 0) {
  1012. top2X = pos2 + angle2*textLine.getCharAscent(logIndex);
  1013. bottom2X = pos2 - angle2*textLine.getCharDescent(logIndex);
  1014. }
  1015. else {
  1016. top2X = bottom2X = pos2;
  1017. }
  1018. }
  1019. }
  1020. float topX = (top1X + top2X) / 2;
  1021. float bottomX = (bottom1X + bottom2X) / 2;
  1022. float[] info = new float[2];
  1023. if (isVerticalLine) {
  1024. info[1] = (float) ((topX - bottomX) / bounds.getWidth());
  1025. info[0] = (float) (topX + (info[1]*bounds.getX()));
  1026. }
  1027. else {
  1028. info[1] = (float) ((topX - bottomX) / bounds.getHeight());
  1029. info[0] = (float) (bottomX + (info[1]*bounds.getMaxY()));
  1030. }
  1031. return info;
  1032. }
  1033. /**
  1034. * Returns information about the caret corresponding to <code>hit</code>.
  1035. * The first element of the array is the intersection of the caret with
  1036. * the baseline. The second element of the array is the slope (run/rise)
  1037. * of the caret.
  1038. * <p>
  1039. * This method is meant for informational use. To display carets, it
  1040. * is better to use <code>getCaretShapes</code>.
  1041. * @param hit a hit on a character in this <code>TextLayout</code>
  1042. * @param bounds the bounds to which the caret info is constructed
  1043. * @return a two-element array containing the position and slope of
  1044. * the caret.
  1045. * @see #getCaretShapes(int, Rectangle2D, TextLayout.CaretPolicy)
  1046. */
  1047. public float[] getCaretInfo(TextHitInfo hit, Rectangle2D bounds) {
  1048. ensureCache();
  1049. checkTextHit(hit);
  1050. return getCaretInfo(hitToCaret(hit), bounds);
  1051. }
  1052. /**
  1053. * Returns information about the caret corresponding to <code>hit</code>.
  1054. * This method is a convenience overload of <code>getCaretInfo</code> and
  1055. * uses the natural bounds of this <code>TextLayout</code>.
  1056. * @param hit a hit on a character in this <code>TextLayout</code>
  1057. * @return the information about a caret corresponding to a hit.
  1058. */
  1059. public float[] getCaretInfo(TextHitInfo hit) {
  1060. return getCaretInfo(hit, getNaturalBounds());
  1061. }
  1062. /**
  1063. * Returns a caret index corresponding to <code>hit</code>.
  1064. * Carets are numbered from left to right (top to bottom) starting from
  1065. * zero. This always places carets next to the character hit, on the
  1066. * indicated side of the character.
  1067. * @param hit a hit on a character in this <code>TextLayout</code>
  1068. * @return a caret index corresponding to the specified hit.
  1069. */
  1070. private int hitToCaret(TextHitInfo hit) {
  1071. int hitIndex = hit.getCharIndex();
  1072. if (hitIndex < 0) {
  1073. return textLine.isDirectionLTR() ? 0 : characterCount;
  1074. } else if (hitIndex >= characterCount) {
  1075. return textLine.isDirectionLTR() ? characterCount : 0;
  1076. }
  1077. int visIndex = textLine.logicalToVisual(hitIndex);
  1078. if (hit.isLeadingEdge() != textLine.isCharLTR(hitIndex)) {
  1079. ++visIndex;
  1080. }
  1081. return visIndex;
  1082. }
  1083. /**
  1084. * Given a caret index, return a hit whose caret is at the index.
  1085. * The hit is NOT guaranteed to be strong!!!
  1086. *
  1087. * @param caret a caret index.
  1088. * @return a hit on this layout whose strong caret is at the requested
  1089. * index.
  1090. */
  1091. private TextHitInfo caretToHit(int caret) {
  1092. if (caret == 0 || caret == characterCount) {
  1093. if ((caret == characterCount) == textLine.isDirectionLTR()) {
  1094. return TextHitInfo.leading(characterCount);
  1095. }
  1096. else {
  1097. return TextHitInfo.trailing(-1);
  1098. }
  1099. }
  1100. else {
  1101. int charIndex = textLine.visualToLogical(caret);
  1102. boolean leading = textLine.isCharLTR(charIndex);
  1103. return leading? TextHitInfo.leading(charIndex)
  1104. : TextHitInfo.trailing(charIndex);
  1105. }
  1106. }
  1107. private boolean caretIsValid(int caret) {
  1108. if (caret == characterCount || caret == 0) {
  1109. return true;
  1110. }
  1111. int offset = textLine.visualToLogical(caret);
  1112. if (!textLine.isCharLTR(offset)) {
  1113. offset = textLine.visualToLogical(caret-1);
  1114. if (textLine.isCharLTR(offset)) {
  1115. return true;
  1116. }
  1117. }
  1118. // At this point, the leading edge of the character
  1119. // at offset is at the given caret.
  1120. return textLine.caretAtOffsetIsValid(offset);
  1121. }
  1122. /**
  1123. * Returns the hit for the next caret to the right (bottom); if there
  1124. * is no such hit, returns <code>null</code>.
  1125. * If the hit character index is out of bounds, an
  1126. * {@link IllegalArgumentException} is thrown.
  1127. * @param hit a hit on a character in this layout
  1128. * @return a hit whose caret appears at the next position to the
  1129. * right (bottom) of the caret of the provided hit or <code>null</code>.
  1130. */
  1131. public TextHitInfo getNextRightHit(TextHitInfo hit) {
  1132. ensureCache();
  1133. checkTextHit(hit);
  1134. int caret = hitToCaret(hit);
  1135. if (caret == characterCount) {
  1136. return null;
  1137. }
  1138. do {
  1139. ++caret;
  1140. } while (!caretIsValid(caret));
  1141. return caretToHit(caret);
  1142. }
  1143. /**
  1144. * Returns the hit for the next caret to the right (bottom); if no
  1145. * such hit, returns <code>null</code>. The hit is to the right of
  1146. * the strong caret at the specified offset, as determined by the
  1147. * specified policy.
  1148. * The returned hit is the stronger of the two possible
  1149. * hits, as determined by the specified policy.
  1150. * @param offset an insertion offset in this <code>TextLayout</code>.
  1151. * Cannot be less than 0 or greater than this <code>TextLayout</code>
  1152. * object's character count.
  1153. * @param policy the policy used to select the strong caret
  1154. * @return a hit whose caret appears at the next position to the
  1155. * right (bottom) of the caret of the provided hit, or <code>null</code>.
  1156. */
  1157. public TextHitInfo getNextRightHit(int offset, CaretPolicy policy) {
  1158. if (offset < 0 || offset > characterCount) {
  1159. throw new IllegalArgumentException("Offset out of bounds in TextLayout.getNextRightHit()");
  1160. }
  1161. if (policy == null) {
  1162. throw new IllegalArgumentException("Null CaretPolicy passed to TextLayout.getNextRightHit()");
  1163. }
  1164. TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
  1165. TextHitInfo hit2 = hit1.getOtherHit();
  1166. TextHitInfo nextHit = getNextRightHit(policy.getStrongCaret(hit1, hit2, this));
  1167. if (nextHit != null) {
  1168. TextHitInfo otherHit = getVisualOtherHit(nextHit);
  1169. return policy.getStrongCaret(otherHit, nextHit, this);
  1170. }
  1171. else {
  1172. return null;
  1173. }
  1174. }
  1175. /**
  1176. * Returns the hit for the next caret to the right (bottom); if no
  1177. * such hit, returns <code>null</code>. The hit is to the right of
  1178. * the strong caret at the specified offset, as determined by the
  1179. * default policy.
  1180. * The returned hit is the stronger of the two possible
  1181. * hits, as determined by the default policy.
  1182. * @param offset an insertion offset in this <code>TextLayout</code>.
  1183. * Cannot be less than 0 or greater than the <code>TextLayout</code>
  1184. * object's character count.
  1185. * @return a hit whose caret appears at the next position to the
  1186. * right (bottom) of the caret of the provided hit, or <code>null</code>.
  1187. */
  1188. public TextHitInfo getNextRightHit(int offset) {
  1189. return getNextRightHit(offset, DEFAULT_CARET_POLICY);
  1190. }
  1191. /**
  1192. * Returns the hit for the next caret to the left (top); if no such
  1193. * hit, returns <code>null</code>.
  1194. * If the hit character index is out of bounds, an
  1195. * <code>IllegalArgumentException</code> is thrown.
  1196. * @param hit a hit on a character in this <code>TextLayout</code>.
  1197. * @return a hit whose caret appears at the next position to the
  1198. * left (top) of the caret of the provided hit, or <code>null</code>.
  1199. */
  1200. public TextHitInfo getNextLeftHit(TextHitInfo hit) {
  1201. ensureCache();
  1202. checkTextHit(hit);
  1203. int caret = hitToCaret(hit);
  1204. if (caret == 0) {
  1205. return null;
  1206. }
  1207. do {
  1208. --caret;
  1209. } while(!caretIsValid(caret));
  1210. return caretToHit(caret);
  1211. }
  1212. /**
  1213. * Returns the hit for the next caret to the left (top); if no
  1214. * such hit, returns <code>null</code>. The hit is to the left of
  1215. * the strong caret at the specified offset, as determined by the
  1216. * specified policy.
  1217. * The returned hit is the stronger of the two possible
  1218. * hits, as determined by the specified policy.
  1219. * @param offset an insertion offset in this <code>TextLayout</code>.
  1220. * Cannot be less than 0 or greater than this <code>TextLayout</code>
  1221. * object's character count.
  1222. * @param policy the policy used to select the strong caret
  1223. * @return a hit whose caret appears at the next position to the
  1224. * left (top) of the caret of the provided hit, or <code>null</code>.
  1225. */
  1226. public TextHitInfo getNextLeftHit(int offset, CaretPolicy policy) {
  1227. if (policy == null) {
  1228. throw new IllegalArgumentException("Null CaretPolicy passed to TextLayout.getNextLeftHit()");
  1229. }
  1230. if (offset < 0 || offset > characterCount) {
  1231. throw new IllegalArgumentException("Offset out of bounds in TextLayout.getNextLeftHit()");
  1232. }
  1233. TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
  1234. TextHitInfo hit2 = hit1.getOtherHit();
  1235. TextHitInfo nextHit = getNextLeftHit(policy.getStrongCaret(hit1, hit2, this));
  1236. if (nextHit != null) {
  1237. TextHitInfo otherHit = getVisualOtherHit(nextHit);
  1238. return policy.getStrongCaret(otherHit, nextHit, this);
  1239. }
  1240. else {
  1241. return null;
  1242. }
  1243. }
  1244. /**
  1245. * Returns the hit for the next caret to the left (top); if no
  1246. * such hit, returns <code>null</code>. The hit is to the left of
  1247. * the strong caret at the specified offset, as determined by the
  1248. * default policy.
  1249. * The returned hit is the stronger of the two possible
  1250. * hits, as determined by the default policy.
  1251. * @param offset an insertion offset in this <code>TextLayout</code>.
  1252. * Cannot be less than 0 or greater than this <code>TextLayout</code>
  1253. * object's character count.
  1254. * @return a hit whose caret appears at the next position to the
  1255. * left (top) of the caret of the provided hit, or <code>null</code>.
  1256. */
  1257. public TextHitInfo getNextLeftHit(int offset) {
  1258. return getNextLeftHit(offset, DEFAULT_CARET_POLICY);
  1259. }
  1260. /**
  1261. * Returns the hit on the opposite side of the specified hit's caret.
  1262. * @param hit the specified hit
  1263. * @return a hit that is on the opposite side of the specified hit's
  1264. * caret.
  1265. */
  1266. public TextHitInfo getVisualOtherHit(TextHitInfo hit) {
  1267. ensureCache();
  1268. checkTextHit(hit);
  1269. int hitCharIndex = hit.getCharIndex();
  1270. int charIndex;
  1271. boolean leading;
  1272. if (hitCharIndex == -1 || hitCharIndex == characterCount) {
  1273. int visIndex;
  1274. if (textLine.isDirectionLTR() == (hitCharIndex == -1)) {
  1275. visIndex = 0;
  1276. }
  1277. else {
  1278. visIndex = characterCount-1;
  1279. }
  1280. charIndex = textLine.visualToLogical(visIndex);
  1281. if (textLine.isDirectionLTR() == (hitCharIndex == -1)) {
  1282. // at left end
  1283. leading = textLine.isCharLTR(charIndex);
  1284. }
  1285. else {
  1286. // at right end
  1287. leading = !textLine.isCharLTR(charIndex);
  1288. }
  1289. }
  1290. else {
  1291. int visIndex = textLine.logicalToVisual(hitCharIndex);
  1292. boolean movedToRight;
  1293. if (textLine.isCharLTR(hitCharIndex) == hit.isLeadingEdge()) {
  1294. --visIndex;
  1295. movedToRight = false;
  1296. }
  1297. else {
  1298. ++visIndex;
  1299. movedToRight = true;
  1300. }
  1301. if (visIndex > -1 && visIndex < characterCount) {
  1302. charIndex = textLine.visualToLogical(visIndex);
  1303. leading = movedToRight == textLine.isCharLTR(charIndex);
  1304. }
  1305. else {
  1306. charIndex =
  1307. (movedToRight == textLine.isDirectionLTR())? characterCount : -1;
  1308. leading = charIndex == characterCount;
  1309. }
  1310. }
  1311. return leading? TextHitInfo.leading(charIndex) :
  1312. TextHitInfo.trailing(charIndex);
  1313. }
  1314. /**
  1315. * Return an array of four floats corresponding the endpoints of the caret
  1316. * x0, y0, x1, y1.
  1317. *
  1318. * This creates a line along the slope of the caret intersecting the
  1319. * baseline at the caret
  1320. * position, and extending from ascent above the baseline to descent below
  1321. * it.
  1322. */
  1323. private double[] getCaretPath(int caret, Rectangle2D bounds,
  1324. boolean clipToBounds) {
  1325. float[] info = getCaretInfo(caret, bounds);
  1326. double pos = info[0];
  1327. double slope = info[1];
  1328. double x0, y0, x1, y1;
  1329. double x2 = -3141.59, y2 = -2.7; // values are there to make compiler happy
  1330. double left = bounds.getX();
  1331. double right = left + bounds.getWidth();
  1332. double top = bounds.getY();
  1333. double bottom = top + bounds.getHeight();
  1334. boolean threePoints = false;
  1335. if (isVerticalLine) {
  1336. if (slope >= 0) {
  1337. x0 = left;
  1338. x1 = right;
  1339. y0 = pos + x0 * slope;
  1340. y1 = pos + x1 * slope;
  1341. }
  1342. else {
  1343. x1 = left;
  1344. x0 = right;
  1345. y1 = pos + x1 * slope;
  1346. y0 = pos + x0 * slope;
  1347. }
  1348. // y0 <= y1, always
  1349. if (clipToBounds) {
  1350. if (y0 < top) {
  1351. if (slope == 0 || y1 <= top) {
  1352. y0 = y1 = top;
  1353. }
  1354. else {
  1355. threePoints = true;
  1356. y2 = top;
  1357. x2 = x0 + (top-y0)/slope;
  1358. y0 = top;
  1359. if (y1 > bottom)
  1360. y1 = bottom;
  1361. }
  1362. }
  1363. else if (y1 > bottom) {
  1364. if (slope == 0 || y0 >= bottom) {
  1365. y0 = y1 = bottom;
  1366. }
  1367. else {
  1368. threePoints = true;
  1369. y2 = bottom;
  1370. x2 = x1 - (y1-bottom)/slope;
  1371. y1 = bottom;
  1372. }
  1373. }
  1374. }
  1375. }
  1376. else {
  1377. if (slope >= 0) {
  1378. y0 = bottom;
  1379. y1 = top;
  1380. x0 = pos - y0 * slope;
  1381. x1 = pos - y1 * slope;
  1382. }
  1383. else {
  1384. y1 = bottom;
  1385. y0 = top;
  1386. x1 = pos - y0 * slope;
  1387. x0 = pos - y1 * slope;
  1388. }
  1389. // x0 <= x1, always
  1390. if (clipToBounds) {
  1391. if (x0 < left) {
  1392. if (slope == 0 || x1 <= left) {
  1393. x0 = x1 = left;
  1394. }
  1395. else {
  1396. threePoints = true;
  1397. x2 = left;
  1398. y2 = y0 - (left-x0)/slope;
  1399. x0 = left;
  1400. if (x1 > right)
  1401. x1 = right;
  1402. }
  1403. }
  1404. else if (x1 > right) {
  1405. if (slope == 0 || x0 >= right) {
  1406. x0 = x1 = right;
  1407. }
  1408. else {
  1409. threePoints = true;
  1410. x2 = right;
  1411. y2 = y1 + (x1-right)/slope;
  1412. x1 = right;
  1413. }
  1414. }
  1415. }
  1416. }
  1417. return threePoints?
  1418. new double[] { x0, y0, x2, y2, x1, y1 } :
  1419. new double[] { x0, y0, x1, y1 };
  1420. }
  1421. private static GeneralPath pathToShape(double[] path, boolean close) {
  1422. GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD, path.length);
  1423. result.moveTo((float)path[0], (float)path[1]);
  1424. for (int i = 2; i < path.length; i += 2) {
  1425. result.lineTo((float)path[i], (float)path[i+1]);
  1426. }
  1427. if (close) {
  1428. result.closePath();
  1429. }
  1430. return result;
  1431. }
  1432. /**
  1433. * Returns a {@link Shape} representing the caret at the specified
  1434. * hit inside the specified bounds.
  1435. * @param hit the hit at which to generate the caret
  1436. * @param bounds the bounds of the <code>TextLayout</code> to use
  1437. * in generating the caret.
  1438. * @return a <code>Shape</code> representing the caret.
  1439. */
  1440. public Shape getCaretShape(TextHitInfo hit, Rectangle2D bounds) {
  1441. checkTextHit(hit);
  1442. if (bounds == null) {
  1443. throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getCaret()");
  1444. }
  1445. int hitCaret = hitToCaret(hit);
  1446. GeneralPath hitShape =
  1447. pathToShape(getCaretPath(hitCaret, bounds, false), false);
  1448. //return new Highlight(hitShape, true);
  1449. return hitShape;
  1450. }
  1451. /**
  1452. * Returns a <code>Shape</code> representing the caret at the specified
  1453. * hit inside the natural bounds of this <code>TextLayout</code>.
  1454. * @param hit the hit at which to generate the caret
  1455. * @return a <code>Shape</code> representing the caret.
  1456. */
  1457. public Shape getCaretShape(TextHitInfo hit) {
  1458. return getCaretShape(hit, getNaturalBounds());
  1459. }
  1460. /**
  1461. * Return the "stronger" of the TextHitInfos. The TextHitInfos
  1462. * should be logical or visual counterparts. They are not
  1463. * checked for validity.
  1464. */
  1465. private final TextHitInfo getStrongHit(TextHitInfo hit1, TextHitInfo hit2) {
  1466. // right now we're using the following rule for strong hits:
  1467. // A hit on a character with a lower level
  1468. // is stronger than one on a character with a higher level.
  1469. // If this rule ties, the hit on the leading edge of a character wins.
  1470. // If THIS rule ties, hit1 wins. Both rules shouldn't tie, unless the
  1471. // infos aren't counterparts of some sort.
  1472. byte hit1Level = getCharacterLevel(hit1.getCharIndex());
  1473. byte hit2Level = getCharacterLevel(hit2.getCharIndex());
  1474. if (hit1Level == hit2Level) {
  1475. if (hit2.isLeadingEdge() && !hit1.isLeadingEdge()) {
  1476. return hit2;
  1477. }
  1478. else {
  1479. return hit1;
  1480. }
  1481. }
  1482. else {
  1483. return (hit1Level < hit2Level)? hit1 : hit2;
  1484. }
  1485. }
  1486. /**
  1487. * Returns the level of the character at <code>index</code>.
  1488. * Indices -1 and <code>characterCount</code> are assigned the base
  1489. * level of this <code>TextLayout</code>.
  1490. * @param index the index of the character from which to get the level
  1491. * @return the level of the character at the specified index.
  1492. */
  1493. public byte getCharacterLevel(int index) {
  1494. // hmm, allow indices at endpoints? For now, yes.
  1495. if (index == -1 || index == characterCount) {
  1496. return (byte) (textLine.isDirectionLTR()? 0 : 1);
  1497. }
  1498. if (index < 0 || index >= characterCount) {
  1499. throw new IllegalArgumentException("Index is out of range in getCharacterLevel.");
  1500. }
  1501. return textLine.getCharLevel(index);
  1502. }
  1503. /**
  1504. * Returns two paths corresponding to the strong and weak caret.
  1505. * @param offset an offset in this <code>TextLayout</code>
  1506. * @param bounds the bounds to which to extend the carets
  1507. * @param policy the specified <code>CaretPolicy</code>
  1508. * @return an array of two paths. Element zero is the strong
  1509. * caret. If there are two carets, element one is the weak caret,
  1510. * otherwise it is <code>null</code>.
  1511. */
  1512. public Shape[] getCaretShapes(int offset, Rectangle2D bounds, CaretPolicy policy) {
  1513. ensureCache();
  1514. if (offset < 0 || offset > characterCount) {
  1515. throw new IllegalArgumentException("Offset out of bounds in TextLayout.getCaretShapes()");
  1516. }
  1517. if (bounds == null) {
  1518. throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getCaretShapes()");
  1519. }
  1520. if (policy == null) {
  1521. throw new IllegalArgumentException("Null CaretPolicy passed to TextLayout.getCaretShapes()");
  1522. }
  1523. Shape[] result = new Shape[2];
  1524. TextHitInfo hit = TextHitInfo.afterOffset(offset);
  1525. int hitCaret = hitToCaret(hit);
  1526. Shape hitShape =
  1527. pathToShape(getCaretPath(hitCaret, bounds, false), false);
  1528. TextHitInfo otherHit = hit.getOtherHit();
  1529. int otherCaret = hitToCaret(otherHit);
  1530. if (hitCaret == otherCaret) {
  1531. result[0] = hitShape;
  1532. }
  1533. else { // more than one caret
  1534. Shape otherShape =
  1535. pathToShape(getCaretPath(otherCaret, bounds, false), false);
  1536. TextHitInfo strongHit = policy.getStrongCaret(hit, otherHit, this);
  1537. boolean hitIsStrong = strongHit.equals(hit);
  1538. if (hitIsStrong) {// then other is weak
  1539. result[0] = hitShape;
  1540. result[1] = otherShape;
  1541. }
  1542. else {
  1543. result[0] = otherShape;
  1544. result[1] = hitShape;
  1545. }
  1546. }
  1547. return result;
  1548. }
  1549. /**
  1550. * Returns two paths corresponding to the strong and weak caret.
  1551. * This method is a convenience overload of <code>getCaretShapes</code>
  1552. * that uses the default caret policy.
  1553. * @param offset an offset in this <code>TextLayout</code>
  1554. * @param bounds the bounds to which to extend the carets
  1555. * @return two paths corresponding to the strong and weak caret as
  1556. * defined by the <code>DEFAULT_CARET_POLICY</code>
  1557. */
  1558. public Shape[] getCaretShapes(int offset, Rectangle2D bounds) {
  1559. // {sfb} parameter checking is done in overloaded version
  1560. return getCaretShapes(offset, bounds, DEFAULT_CARET_POLICY);
  1561. }
  1562. /**
  1563. * Returns two paths corresponding to the strong and weak caret.
  1564. * This method is a convenience overload of <code>getCaretShapes</code>
  1565. * that uses the default caret policy and this <code>TextLayout</code>
  1566. * object's natural bounds.
  1567. * @param offset an offset in this <code>TextLayout</code>
  1568. * @param bounds the bounds to which to extend the carets
  1569. * @return two paths corresponding to the strong and weak caret as
  1570. * defined by the <code>DEFAULT_CARET_POLICY</code>
  1571. */
  1572. public Shape[] getCaretShapes(int offset) {
  1573. // {sfb} parameter checking is done in overloaded version
  1574. return getCaretShapes(offset, getNaturalBounds(), DEFAULT_CARET_POLICY);
  1575. }
  1576. // A utility to return a path enclosing the given path
  1577. // Path0 must be left or top of path1
  1578. // {jbr} no assumptions about size of path0, path1 anymore.
  1579. private static GeneralPath boundingShape(double[] path0, double[] path1) {
  1580. GeneralPath result = pathToShape(path0, false);
  1581. for (int i = path1.length-2; i >=0; i -= 2) {
  1582. result.lineTo((float)path1[i], (float)path1[i+1]);
  1583. }
  1584. result.closePath();
  1585. return result;
  1586. }
  1587. // A utility to convert a pair of carets into a bounding path
  1588. // {jbr} Shape is never outside of bounds.
  1589. private GeneralPath caretBoundingShape(int caret0,
  1590. int caret1,
  1591. Rectangle2D bounds) {
  1592. if (caret0 > caret1) {
  1593. int temp = caret0;
  1594. caret0 = caret1;
  1595. caret1 = temp;
  1596. }
  1597. return boundingShape(getCaretPath(caret0, bounds, true),
  1598. getCaretPath(caret1, bounds, true));
  1599. }
  1600. /*
  1601. * A utility to return the path bounding the area to the left (top) of the
  1602. * layout.
  1603. * Shape is never outside of bounds.
  1604. */
  1605. private GeneralPath leftShape(Rectangle2D bounds) {
  1606. double[] path0;
  1607. if (isVerticalLine) {
  1608. path0 = new double[] { bounds.getX(), bounds.getY(),
  1609. bounds.getX() + bounds.getWidth(),
  1610. bounds.getY() };
  1611. } else {
  1612. path0 = new double[] { bounds.getX(),
  1613. bounds.getY() + bounds.getHeight(),
  1614. bounds.getX(), bounds.getY() };
  1615. }
  1616. double[] path1 = getCaretPath(0, bounds, true);
  1617. return boundingShape(path0, path1);
  1618. }
  1619. /*
  1620. * A utility to return the path bounding the area to the right (bottom) of
  1621. * the layout.
  1622. */
  1623. private GeneralPath rightShape(Rectangle2D bounds) {
  1624. double[] path1;
  1625. if (isVerticalLine) {
  1626. path1 = new double[] {
  1627. bounds.getX(),
  1628. bounds.getY() + bounds.getHeight(),
  1629. bounds.getX() + bounds.getWidth(),
  1630. bounds.getY() + bounds.getHeight()
  1631. };
  1632. } else {
  1633. path1 = new double[] {
  1634. bounds.getX() + bounds.getWidth(),
  1635. bounds.getY() + bounds.getHeight(),
  1636. bounds.getX() + bounds.getWidth(),
  1637. bounds.getY()
  1638. };
  1639. }
  1640. double[] path0 = getCaretPath(characterCount, bounds, true);
  1641. return boundingShape(path0, path1);
  1642. }
  1643. /**
  1644. * Returns the logical ranges of text corresponding to a visual selection.
  1645. * @param firstEndpoint an endpoint of the visual range
  1646. * @param secondEndpoint the other endpoint of the visual range.
  1647. * This endpoint can be less than <code>firstEndpoint</code>.
  1648. * @return an array of integers representing start/limit pairs for the
  1649. * selected ranges.
  1650. * @see #getVisualHighlightShape(TextHitInfo, TextHitInfo, Rectangle2D)
  1651. */
  1652. public int[] getLogicalRangesForVisualSelection(TextHitInfo firstEndpoint,
  1653. TextHitInfo secondEndpoint) {
  1654. ensureCache();
  1655. checkTextHit(firstEndpoint);
  1656. checkTextHit(secondEndpoint);
  1657. // !!! probably want to optimize for all LTR text
  1658. boolean[] included = new boolean[characterCount];
  1659. int startIndex = hitToCaret(firstEndpoint);
  1660. int limitIndex = hitToCaret(secondEndpoint);
  1661. if (startIndex > limitIndex) {
  1662. int t = startIndex;
  1663. startIndex = limitIndex;
  1664. limitIndex = t;
  1665. }
  1666. /*
  1667. * now we have the visual indexes of the glyphs at the start and limit
  1668. * of the selection range walk through runs marking characters that
  1669. * were included in the visual range there is probably a more efficient
  1670. * way to do this, but this ought to work, so hey
  1671. */
  1672. if (startIndex < limitIndex) {
  1673. int visIndex = startIndex;
  1674. while (visIndex < limitIndex) {
  1675. included[textLine.visualToLogical(visIndex)] = true;
  1676. ++visIndex;
  1677. }
  1678. }
  1679. /*
  1680. * count how many runs we have, ought to be one or two, but perhaps
  1681. * things are especially weird
  1682. */
  1683. int count = 0;
  1684. boolean inrun = false;
  1685. for (int i = 0; i < characterCount; i++) {
  1686. if (included[i] != inrun) {
  1687. inrun = !inrun;
  1688. if (inrun) {
  1689. count++;
  1690. }
  1691. }
  1692. }
  1693. int[] ranges = new int[count * 2];
  1694. count = 0;
  1695. inrun = false;
  1696. for (int i = 0; i < characterCount; i++) {
  1697. if (included[i] != inrun) {
  1698. ranges[count++] = i;
  1699. inrun = !inrun;
  1700. }
  1701. }
  1702. if (inrun) {
  1703. ranges[count++] = characterCount;
  1704. }
  1705. return ranges;
  1706. }
  1707. /**
  1708. * Returns a path enclosing the visual selection in the specified range,
  1709. * extended to <code>bounds</code>.
  1710. * <p>
  1711. * If the selection includes the leftmost (topmost) position, the selection
  1712. * is extended to the left (top) of <code>bounds</code>. If the
  1713. * selection includes the rightmost (bottommost) position, the selection
  1714. * is extended to the right (bottom) of the bounds. The height
  1715. * (width on vertical lines) of the selection is always extended to
  1716. * <code>bounds</code>.
  1717. * <p>
  1718. * Although the selection is always contiguous, the logically selected
  1719. * text can be discontiguous on lines with mixed-direction text. The
  1720. * logical ranges of text selected can be retrieved using
  1721. * <code>getLogicalRangesForVisualSelection</code>. For example,
  1722. * consider the text 'ABCdef' where capital letters indicate
  1723. * right-to-left text, rendered on a right-to-left line, with a visual
  1724. * selection from 0L (the leading edge of 'A') to 3T (the trailing edge
  1725. * of 'd'). The text appears as follows, with bold underlined areas
  1726. * representing the selection:
  1727. * <br><pre>
  1728. * d<u><b>efCBA </b></u>
  1729. * </pre>
  1730. * The logical selection ranges are 0-3, 4-6 (ABC, ef) because the
  1731. * visually contiguous text is logically discontiguous. Also note that
  1732. * since the rightmost position on the layout (to the right of 'A') is
  1733. * selected, the selection is extended to the right of the bounds.
  1734. * @param firstEndpoint one end of the visual selection
  1735. * @param secondEndpoint the other end of the visual selection
  1736. * @param bounds the bounding rectangle to which to extend the selection
  1737. * @return a <code>Shape</code> enclosing the selection.
  1738. * @see #getLogicalRangesForVisualSelection(TextHitInfo, TextHitInfo)
  1739. * @see #getLogicalHighlightShape(int, int, Rectangle2D)
  1740. */
  1741. public Shape getVisualHighlightShape(TextHitInfo firstEndpoint,
  1742. TextHitInfo secondEndpoint,
  1743. Rectangle2D bounds)
  1744. {
  1745. ensureCache();
  1746. checkTextHit(firstEndpoint);
  1747. checkTextHit(secondEndpoint);
  1748. if(bounds == null) {
  1749. throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getVisualHighlightShape()");
  1750. }
  1751. GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
  1752. int firstCaret = hitToCaret(firstEndpoint);
  1753. int secondCaret = hitToCaret(secondEndpoint);
  1754. result.append(caretBoundingShape(firstCaret, secondCaret, bounds),
  1755. false);
  1756. if (firstCaret == 0 || secondCaret == 0) {
  1757. result.append(leftShape(bounds), false);
  1758. }
  1759. if (firstCaret == characterCount || secondCaret == characterCount) {
  1760. result.append(rightShape(bounds), false);
  1761. }
  1762. //return new Highlight(result, false);
  1763. return result;
  1764. }
  1765. /**
  1766. * Returns a <code>Shape</code> enclosing the visual selection in the
  1767. * specified range, extended to the bounds. This method is a
  1768. * convenience overload of <code>getVisualHighlightShape</code> that
  1769. * uses the natural bounds of this <code>TextLayout</code>.
  1770. * @param firstEndpoint one end of the visual selection
  1771. * @param secondEndpoint the other end of the visual selection
  1772. * @return a <code>Shape</code> enclosing the selection.
  1773. */
  1774. public Shape getVisualHighlightShape(TextHitInfo firstEndpoint,
  1775. TextHitInfo secondEndpoint) {
  1776. return getVisualHighlightShape(firstEndpoint, secondEndpoint, getNaturalBounds());
  1777. }
  1778. /**
  1779. * Returns a <code>Shape</code> enclosing the logical selection in the
  1780. * specified range, extended to the specified <code>bounds</code>.
  1781. * <p>
  1782. * If the selection range includes the first logical character, the
  1783. * selection is extended to the portion of <code>bounds</code> before
  1784. * the start of this <code>TextLayout</code>. If the range includes
  1785. * the last logical character, the selection is extended to the portion
  1786. * of <code>bounds</code> after the end of this <code>TextLayout</code>.
  1787. * The height (width on vertical lines) of the selection is always
  1788. * extended to <code>bounds</code>.
  1789. * <p>
  1790. * The selection can be discontiguous on lines with mixed-direction text.
  1791. * Only those characters in the logical range between start and limit
  1792. * appear selected. For example, consider the text 'ABCdef' where capital
  1793. * letters indicate right-to-left text, rendered on a right-to-left line,
  1794. * with a logical selection from 0 to 4 ('ABCd'). The text appears as
  1795. * follows, with bold standing in for the selection, and underlining for
  1796. * the extension:
  1797. * <br><pre>
  1798. * <u><b>d</b></u>ef<u><b>CBA </b></u>
  1799. * </pre>
  1800. * The selection is discontiguous because the selected characters are
  1801. * visually discontiguous. Also note that since the range includes the
  1802. * first logical character (A), the selection is extended to the portion
  1803. * of the <code>bounds</code> before the start of the layout, which in
  1804. * this case (a right-to-left line) is the right portion of the
  1805. * <code>bounds</code>.
  1806. * @param firstEndpoint an endpoint in the range of characters to select
  1807. * @param secondEndpoint the other endpoint of the range of characters
  1808. * to select. Can be less than <code>firstEndpoint</code>. The range
  1809. * includes the character at min(firstEndpoint, secondEndpoint), but
  1810. * excludes max(firstEndpoint, secondEndpoint).
  1811. * @param bounds the bounding rectangle to which to extend the selection
  1812. * @return an area enclosing the selection.
  1813. * @see #getVisualHighlightShape(TextHitInfo, TextHitInfo, Rectangle2D)
  1814. */
  1815. public Shape getLogicalHighlightShape(int firstEndpoint,
  1816. int secondEndpoint,
  1817. Rectangle2D bounds) {
  1818. if (bounds == null) {
  1819. throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getLogicalHighlightShape()");
  1820. }
  1821. ensureCache();
  1822. if (firstEndpoint > secondEndpoint) {
  1823. int t = firstEndpoint;
  1824. firstEndpoint = secondEndpoint;
  1825. secondEndpoint = t;
  1826. }
  1827. if(firstEndpoint < 0 || secondEndpoint > characterCount) {
  1828. throw new IllegalArgumentException("Range is invalid in TextLayout.getLogicalHighlightShape()");
  1829. }
  1830. int[] carets = new int[10]; // would this ever not handle all cases?
  1831. int count = 0;
  1832. if (firstEndpoint < secondEndpoint) {
  1833. int logIndex = firstEndpoint;
  1834. do {
  1835. carets[count++] = hitToCaret(TextHitInfo.leading(logIndex));
  1836. boolean ltr = textLine.isCharLTR(logIndex);
  1837. do {
  1838. logIndex++;
  1839. } while (logIndex < secondEndpoint && textLine.isCharLTR(logIndex) == ltr);
  1840. int hitCh = logIndex;
  1841. carets[count++] = hitToCaret(TextHitInfo.trailing(hitCh - 1));
  1842. if (count == carets.length) {
  1843. int[] temp = new int[carets.length + 10];
  1844. System.arraycopy(carets, 0, temp, 0, count);
  1845. carets = temp;
  1846. }
  1847. } while (logIndex < secondEndpoint);
  1848. }
  1849. else {
  1850. count = 2;
  1851. carets[0] = carets[1] = hitToCaret(TextHitInfo.leading(firstEndpoint));
  1852. }
  1853. // now create paths for pairs of carets
  1854. GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
  1855. for (int i = 0; i < count; i += 2) {
  1856. result.append(caretBoundingShape(carets[i], carets[i+1], bounds),
  1857. false);
  1858. }
  1859. if ((textLine.isDirectionLTR() && firstEndpoint == 0) || (!textLine.isDirectionLTR() &&
  1860. secondEndpoint == characterCount)) {
  1861. result.append(leftShape(bounds), false);
  1862. }
  1863. if ((textLine.isDirectionLTR() && secondEndpoint == characterCount) ||
  1864. (!textLine.isDirectionLTR() && firstEndpoint == 0)) {
  1865. result.append(rightShape(bounds), false);
  1866. }
  1867. return result;
  1868. }
  1869. /**
  1870. * Returns a <code>Shape</code> enclosing the logical selection in the
  1871. * specified range, extended to the natural bounds of this
  1872. * <code>TextLayout</code>. This method is a convenience overload of
  1873. * <code>getLogicalHighlightShape</code> that uses the natural bounds of
  1874. * this <code>TextLayout</code>.
  1875. * @param firstEndpoint an endpoint in the range of characters to select
  1876. * @param secondEndpoint the other endpoint of the range of characters
  1877. * to select. Can be less than <code>firstEndpoint</code>. The range
  1878. * includes the character at min(firstEndpoint, secondEndpoint), but
  1879. * excludes max(firstEndpoint, secondEndpoint).
  1880. * @return a <code>Shape</code> enclosing the selection.
  1881. */
  1882. public Shape getLogicalHighlightShape(int firstEndpoint, int secondEndpoint) {
  1883. return getLogicalHighlightShape(firstEndpoint, secondEndpoint, getNaturalBounds());
  1884. }
  1885. /**
  1886. * Returns the black box bounds of the characters in the specified range.
  1887. * The black box bounds is an area consisting of the union of the bounding
  1888. * boxes of all the glyphs corresponding to the characters between start
  1889. * and limit. This path may be disjoint.
  1890. * @param firstEndpoint one end of the character range
  1891. * @param secondEndpoint the other end of the character range. Can be
  1892. * less than <code>firstEndpoint</code>.
  1893. * @return a <code>path</code> enclosing the black box bounds.
  1894. */
  1895. public Shape getBlackBoxBounds(int firstEndpoint, int secondEndpoint) {
  1896. ensureCache();
  1897. if (firstEndpoint > secondEndpoint) {
  1898. int t = firstEndpoint;
  1899. firstEndpoint = secondEndpoint;
  1900. secondEndpoint = t;
  1901. }
  1902. if (firstEndpoint < 0 || secondEndpoint > characterCount) {
  1903. throw new IllegalArgumentException("Invalid range passed to TextLayout.getBlackBoxBounds()");
  1904. }
  1905. /*
  1906. * return an area that consists of the bounding boxes of all the
  1907. * characters from firstEndpoint to limit
  1908. */
  1909. GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
  1910. if (firstEndpoint < characterCount) {
  1911. for (int logIndex = firstEndpoint;
  1912. logIndex < secondEndpoint;
  1913. logIndex++) {
  1914. if (!textLine.isCharWhitespace(logIndex)) {
  1915. Rectangle2D r = textLine.getCharBounds(logIndex);
  1916. result.append(r, false);
  1917. }
  1918. }
  1919. }
  1920. if (dx != 0 || dy != 0) {
  1921. AffineTransform translate = new AffineTransform();
  1922. translate.setToTranslation(dx, dy);
  1923. result = (GeneralPath) result.createTransformedShape(translate);
  1924. }
  1925. //return new Highlight(result, false);
  1926. return result;
  1927. }
  1928. /**
  1929. * Returns the distance from the point (x, y) to the caret along
  1930. * the line direction defined in <code>caretInfo</code>. Distance is
  1931. * negative if the point is to the left of the caret on a horizontal
  1932. * line, or above the caret on a vertical line.
  1933. * Utility for use by hitTestChar.
  1934. */
  1935. private float caretToPointDistance(float[] caretInfo, float x, float y) {
  1936. // distanceOffBaseline is negative if you're 'above' baseline
  1937. float lineDistance = isVerticalLine? y : x;
  1938. float distanceOffBaseline = isVerticalLine? -x : y;
  1939. return lineDistance - caretInfo[0] +
  1940. (distanceOffBaseline*caretInfo[1]);
  1941. }
  1942. /**
  1943. * Returns a <code>TextHitInfo</code> corresponding to the
  1944. * specified point.
  1945. * Coordinates outside the bounds of the <code>TextLayout</code>
  1946. * map to hits on the leading edge of the first logical character,
  1947. * or the trailing edge of the last logical character, as appropriate,
  1948. * regardless of the position of that character in the line. Only the
  1949. * direction along the baseline is used to make this evaluation.
  1950. * @param x the x offset from the origin of this
  1951. * <code>TextLayout</code>
  1952. * @param y the y offset from the origin of this
  1953. * <code>TextLayout</code>
  1954. * @param bounds the bounds of the <code>TextLayout</code>
  1955. * @return a hit describing the character and edge (leading or trailing)
  1956. * under the specified point.
  1957. */
  1958. public TextHitInfo hitTestChar(float x, float y, Rectangle2D bounds) {
  1959. ensureCache();
  1960. int hitLB, hitUB;
  1961. float[] caretInfo;
  1962. hitLB = 0;
  1963. caretInfo = getCaretInfo(hitLB, bounds);
  1964. if (caretToPointDistance(caretInfo, x, y) < 0) {
  1965. return textLine.isDirectionLTR()? TextHitInfo.trailing(-1) :
  1966. TextHitInfo.leading(characterCount);
  1967. }
  1968. hitUB = characterCount;
  1969. caretInfo = getCaretInfo(hitUB, bounds);
  1970. if (caretToPointDistance(caretInfo, x, y) >= 0) {
  1971. return textLine.isDirectionLTR()? TextHitInfo.leading(characterCount) :
  1972. TextHitInfo.trailing(-1);
  1973. }
  1974. while (true) {
  1975. // if there are no valid caret positions between
  1976. // hitLB and hitUB then exit this loop; otherwise
  1977. // set test to a valid caret position in the
  1978. // interval (hitLB, hitUB)
  1979. if (hitLB + 1 == hitUB)
  1980. break;
  1981. int test = (hitLB + hitUB) / 2;
  1982. while(!caretIsValid(test)) {
  1983. test++;
  1984. }
  1985. if (test == hitUB) {
  1986. // If we're here then there were no valid caret
  1987. // positions between the halfway point and the
  1988. // end of the test region. Reset test and back
  1989. // up to a valid caret position.
  1990. test = (hitLB + hitUB) / 2;
  1991. do {
  1992. --test;
  1993. } while(!caretIsValid(test));
  1994. if (test == hitLB)
  1995. break;
  1996. }
  1997. caretInfo = getCaretInfo(test, bounds);
  1998. float caretDist = caretToPointDistance(caretInfo, x, y);
  1999. if (caretDist == 0) {
  2000. // return a hit on the left side of the glyph at test
  2001. // test is a valid position, since it is less than characterCount
  2002. int charIndex = textLine.visualToLogical(test);
  2003. boolean leading = textLine.isCharLTR(charIndex);
  2004. return leading? TextHitInfo.leading(charIndex) :
  2005. TextHitInfo.trailing(charIndex);
  2006. }
  2007. else if (caretDist < 0) {
  2008. hitUB = test;
  2009. }
  2010. else {
  2011. hitLB = test;
  2012. }
  2013. }
  2014. // now hit char is either to the right of hitLB
  2015. // or left of hitUB
  2016. // make caretInfo be center of glyph:
  2017. int logIndex = textLine.visualToLogical(hitLB);
  2018. float hitAdvance = textLine.getCharLinePosition(logIndex) +
  2019. (isVerticalLine? -dy : -dx);
  2020. // this positions hitAdvance at the "center" of the character(s)
  2021. // between hitLB and hitUB
  2022. {
  2023. int tempLogIndex = textLine.visualToLogical(hitUB - 1);
  2024. if (hitUB - hitLB > 1) {
  2025. float tempPos = textLine.getCharLinePosition(tempLogIndex);
  2026. hitAdvance += (tempPos - hitAdvance) / 2;
  2027. }
  2028. hitAdvance += textLine.getCharAdvance(tempLogIndex) / 2;
  2029. }
  2030. if (caretInfo == null) {
  2031. caretInfo = new float[2];
  2032. }
  2033. caretInfo[0] = hitAdvance;
  2034. caretInfo[1] = textLine.getCharAngle(logIndex);
  2035. if (caretInfo[1] != 0) {
  2036. caretInfo[0] += caretInfo[1] * (isVerticalLine?
  2037. textLine.getCharXPosition(logIndex) :
  2038. textLine.getCharYPosition(logIndex));
  2039. }
  2040. float centerDist = caretToPointDistance(caretInfo, x, y);
  2041. TextHitInfo rval;
  2042. if (centerDist < 0) {
  2043. rval = textLine.isCharLTR(logIndex)?
  2044. TextHitInfo.leading(logIndex) :
  2045. TextHitInfo.trailing(logIndex);
  2046. }
  2047. else {
  2048. logIndex = textLine.visualToLogical(hitUB - 1);
  2049. boolean leading = !textLine.isCharLTR(logIndex);
  2050. rval = leading? TextHitInfo.leading(logIndex) :
  2051. TextHitInfo.trailing(logIndex);
  2052. }
  2053. return rval;
  2054. }
  2055. /**
  2056. * Returns a <code>TextHitInfo</code> corresponding to the
  2057. * specified point. This method is a convenience overload of
  2058. * <code>hitTestChar</code> that uses the natural bounds of this
  2059. * <code>TextLayout</code>.
  2060. * @param x the x offset from the origin of this <code>TextLayout</code>
  2061. * @param y the y offset from the origin of this
  2062. * <code>TextLayout</code>
  2063. * @return a hit describing the character and edge (leading or trailing)
  2064. * under the specified point.
  2065. */
  2066. public TextHitInfo hitTestChar(float x, float y) {
  2067. return hitTestChar(x, y, getNaturalBounds());
  2068. }
  2069. /**
  2070. * Returns the hash code of this <code>TextLayout</code>.
  2071. * @return the hash code of this <code>TextLayout</code>.
  2072. */
  2073. public int hashCode() {
  2074. if (hashCodeCache == 0) {
  2075. hashCodeCache = textLine.hashCode();
  2076. }
  2077. return hashCodeCache;
  2078. }
  2079. /**
  2080. * Returns <code>true</code> if the specified <code>Object</code> is a
  2081. * <code>TextLayout</code> object and if the specified <code>Object</code>
  2082. * equals this <code>TextLayout</code>.
  2083. * @param obj an <code>Object</code> to test for equality
  2084. * @return <code>true</code> if the specified <code>Object</code>
  2085. * equals this <code>TextLayout</code> <code>false</code>
  2086. * otherwise.
  2087. */
  2088. public boolean equals(Object obj) {
  2089. return (obj instanceof TextLayout) && equals((TextLayout)obj);
  2090. }
  2091. /**
  2092. * Returns <code>true</code> if the two layouts are equal.
  2093. * Two layouts are equal if they contain equal glyphvectors in the same order.
  2094. * @param rhs the <code>TextLayout</code> to compare to this
  2095. * <code>TextLayout</code>
  2096. * @return <code>true</code> if the specified <code>TextLayout</code>
  2097. * equals this <code>TextLayout</code>.
  2098. *
  2099. */
  2100. public boolean equals(TextLayout rhs) {
  2101. if (rhs == null) {
  2102. return false;
  2103. }
  2104. if (rhs == this) {
  2105. return true;
  2106. }
  2107. return textLine.equals(rhs.textLine);
  2108. }
  2109. /**
  2110. * Returns debugging information for this <code>TextLayout</code>.
  2111. * @return the <code>textLine</code> of this <code>TextLayout</code>
  2112. * as a <code>String</code>.
  2113. */
  2114. public String toString() {
  2115. return textLine.toString();
  2116. }
  2117. /**
  2118. * Renders this <code>TextLayout</code> at the specified location in
  2119. * the specified {@link java.awt.Graphics2D Graphics2D} context.
  2120. * The origin of the layout is placed at x, y. Rendering may touch
  2121. * any point within <code>getBounds()</code> of this position. This
  2122. * leaves the <code>g2</code> unchanged.
  2123. * @param g2 the <code>Graphics2D</code> context into which to render
  2124. * the layout
  2125. * @param x, y the coordinates of the origin of this
  2126. * <code>TextLayout</code>
  2127. * @see #getBounds()
  2128. */
  2129. public void draw(Graphics2D g2, float x, float y) {
  2130. if (g2 == null) {
  2131. throw new IllegalArgumentException("Null Graphics2D passed to TextLayout.draw()");
  2132. }
  2133. textLine.draw(g2, x - dx, y - dy);
  2134. }
  2135. /**
  2136. * Package-only method for testing ONLY. Please don't abuse.
  2137. */
  2138. TextLine getTextLineForTesting() {
  2139. return textLine;
  2140. }
  2141. /**
  2142. *
  2143. * Return the index of the first character with a different baseline from the
  2144. * character at start, or limit if all characters between start and limit have
  2145. * the same baseline.
  2146. */
  2147. private static int sameBaselineUpTo(Font font, char[] text,
  2148. int start, int limit) {
  2149. byte bl = font.getBaselineFor(text[start++]);
  2150. while (start < limit && font.getBaselineFor(text[start]) == bl) {
  2151. ++start;
  2152. }
  2153. return start;
  2154. }
  2155. static byte getBaselineFromGraphic(GraphicAttribute graphic) {
  2156. byte alignment = (byte) graphic.getAlignment();
  2157. if (alignment == GraphicAttribute.BOTTOM_ALIGNMENT ||
  2158. alignment == GraphicAttribute.TOP_ALIGNMENT) {
  2159. return (byte)GraphicAttribute.ROMAN_BASELINE;
  2160. }
  2161. else {
  2162. return alignment;
  2163. }
  2164. }
  2165. /**
  2166. * Returns a <code>Shape</code> representing the outline of this
  2167. * <code>TextLayout</code>.
  2168. * @param tx an optional {@link AffineTransform} to apply to the
  2169. * outline of this <code>TextLayout</code>.
  2170. * @return a <code>Shape</code> that is the outline of this
  2171. * <code>TextLayout</code>.
  2172. */
  2173. public Shape getOutline(AffineTransform tx) {
  2174. return textLine.getOutline(tx);
  2175. }
  2176. }