1. /*
  2. * @(#)TextMeasurer.java 1.25 00/02/02
  3. *
  4. * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved.
  5. *
  6. * This software is the proprietary information of Sun Microsystems, Inc.
  7. * Use is subject to license terms.
  8. *
  9. */
  10. /*
  11. * (C) Copyright Taligent, Inc. 1996 - 1997, All Rights Reserved
  12. * (C) Copyright IBM Corp. 1996 - 1998, All Rights Reserved
  13. *
  14. * The original version of this source code and documentation is
  15. * copyrighted and owned by Taligent, Inc., a wholly-owned subsidiary
  16. * of IBM. These materials are provided under terms of a License
  17. * Agreement between Taligent and Sun. This technology is protected
  18. * by multiple US and International patents.
  19. *
  20. * This notice and attribution to Taligent may not be removed.
  21. * Taligent is a registered trademark of Taligent, Inc.
  22. *
  23. */
  24. package java.awt.font;
  25. import java.awt.Font;
  26. import java.text.CharacterIterator;
  27. import java.text.AttributedCharacterIterator;
  28. import java.text.AttributedString;
  29. import java.awt.font.FontRenderContext;
  30. import sun.awt.font.TextLineComponent;
  31. import sun.awt.font.TextLabelFactory;
  32. import sun.awt.font.Bidi;
  33. /**
  34. * <code>TextMeasurer</code> provides the primitive operations needed for line
  35. * break: measuring up to a given advance, determining the advance of
  36. * a range of characters, and generating a <code>TextLayout</code> for a range of
  37. * characters. It also provides methods for incremental editing
  38. * of paragraphs.
  39. * <p>
  40. * Most clients will use the more convenient <code>LineBreakMeasurer</code>, which
  41. * implements the standard line break policy (placing as many words as
  42. * will fit on each line).
  43. *
  44. * @author John Raley
  45. * @version 1.25, 02/02/00
  46. * @see LineBreakMeasurer
  47. * @since 1.3
  48. */
  49. public final class TextMeasurer {
  50. private FontRenderContext fFrc;
  51. private int fStart;
  52. // characters in source text
  53. private char[] fChars;
  54. // Bidi for this paragraph
  55. private Bidi fBidi;
  56. // Levels array for chars in this paragraph - needed to reorder
  57. // trailing counterdirectional whitespace
  58. private byte[] fLevels;
  59. // glyph arrays in logical order
  60. private TextLineComponent[] fComponents;
  61. // paragraph data - same across all layouts
  62. private boolean fIsDirectionLTR;
  63. private byte fBaseline;
  64. private float[] fBaselineOffsets;
  65. private float fJustifyRatio = 1;
  66. /**
  67. * Constructs a <code>TextMeasurer</code> from the source text.
  68. * The source text should be a single entire paragraph.
  69. * @param text the source paragraph. Cannot be null.
  70. * @param frc the information about a graphics device which is needed
  71. * to measure the text correctly. Cannot be null.
  72. */
  73. public TextMeasurer(AttributedCharacterIterator text, FontRenderContext frc) {
  74. fFrc = frc;
  75. initAll(text);
  76. }
  77. /**
  78. * Initialize state, including fChars array, direction, and
  79. * fBidi.
  80. */
  81. private void initAll(AttributedCharacterIterator text) {
  82. fStart = text.getBeginIndex();
  83. // extract chars
  84. fChars = new char[text.getEndIndex() - fStart];
  85. int n = 0;
  86. for (char c = text.first(); c != text.DONE; c = text.next()) {
  87. fChars[n++] = c;
  88. }
  89. text.first();
  90. try {
  91. Float justifyLF = (Float)text.getAttribute(TextAttribute.JUSTIFICATION);
  92. if (justifyLF != null) {
  93. fJustifyRatio = justifyLF.floatValue();
  94. if (fJustifyRatio < 0) {
  95. fJustifyRatio = 0;
  96. } else if (fJustifyRatio > 1) {
  97. fJustifyRatio = 1;
  98. }
  99. }
  100. }
  101. catch (ClassCastException e) {
  102. }
  103. fBidi = TextLine.createBidiOnParagraph(text, fChars);
  104. generateComponents(text);
  105. }
  106. /**
  107. * Generate components for the paragraph. fChars, fBidi should have been
  108. * initialized already.
  109. */
  110. private void generateComponents(AttributedCharacterIterator text) {
  111. TextLine.FontSource fontSource = new TextLine.ACIFontSource(text);
  112. TextLabelFactory factory = new TextLabelFactory(fFrc, fChars, fBidi);
  113. int[] charsLtoV = null;
  114. if (fBidi != null) {
  115. charsLtoV = fBidi.getLogicalToVisualMap();
  116. fLevels = fBidi.getLevels();
  117. fIsDirectionLTR = fBidi.isDirectionLTR();
  118. }
  119. else {
  120. fLevels = null;
  121. fIsDirectionLTR = true;
  122. }
  123. fComponents = TextLine.getComponents(
  124. fontSource, fChars, 0, fChars.length, charsLtoV, fLevels, factory);
  125. Font firstFont = fontSource.fontAt(0);
  126. if (firstFont == null) {
  127. firstFont = fontSource.getBestFontAt(0);
  128. }
  129. LineMetrics lm = firstFont.getLineMetrics(fChars, 0, 1, fFrc);
  130. fBaselineOffsets = lm.getBaselineOffsets();
  131. }
  132. private int calcLineBreak(int startPos, float width) {
  133. int tlcIndex;
  134. int tlcStart = 0;
  135. for (tlcIndex = 0; tlcIndex < fComponents.length; tlcIndex++) {
  136. int gaLimit = tlcStart + fComponents[tlcIndex].getNumCharacters();
  137. if (gaLimit > startPos) {
  138. break;
  139. }
  140. else {
  141. tlcStart = gaLimit;
  142. }
  143. }
  144. // tlcStart is now the start of the tlc at tlcIndex
  145. for (; tlcIndex < fComponents.length; tlcIndex++) {
  146. TextLineComponent tlc = fComponents[tlcIndex];
  147. int numCharsInGa = tlc.getNumCharacters();
  148. int lineBreak = tlc.getLineBreakIndex(startPos - tlcStart, width);
  149. if (lineBreak == numCharsInGa) {
  150. width -= tlc.getAdvanceBetween(startPos - tlcStart, lineBreak);
  151. tlcStart += numCharsInGa;
  152. startPos = tlcStart;
  153. }
  154. else {
  155. return tlcStart + lineBreak;
  156. }
  157. }
  158. return fChars.length;
  159. }
  160. /**
  161. * According to the Unicode Bidirectional Behavior specification
  162. * (Unicode Standard 2.0, section 3.11), whitespace at the ends
  163. * of lines which would naturally flow against the base direction
  164. * must be made to flow with the line direction, and moved to the
  165. * end of the line. This method returns the start of the sequence
  166. * of trailing whitespace characters to move to the end of a
  167. * line taken from the given range.
  168. */
  169. private int trailingCdWhitespaceStart(int startPos, int limitPos) {
  170. int cdWsStart = limitPos;
  171. if (fLevels != null) {
  172. // Back up over counterdirectional whitespace
  173. final byte baseLevel = (byte) (fIsDirectionLTR? 0 : 1);
  174. for (cdWsStart = limitPos-1; cdWsStart >= startPos; cdWsStart--) {
  175. if ((fLevels[cdWsStart] % 2) == baseLevel ||
  176. Bidi.getDirectionCode(fChars[cdWsStart]) != Bidi.WS) {
  177. cdWsStart++;
  178. break;
  179. }
  180. }
  181. }
  182. return cdWsStart;
  183. }
  184. private TextLineComponent[] makeComponentsOnRange(int startPos,
  185. int limitPos) {
  186. // sigh I really hate to do this here since it's part of the
  187. // bidi algorithm.
  188. // cdWsStart is the start of the trailing counterdirectional
  189. // whitespace
  190. final int cdWsStart = trailingCdWhitespaceStart(startPos, limitPos);
  191. int tlcIndex;
  192. int tlcStart = 0;
  193. for (tlcIndex = 0; tlcIndex < fComponents.length; tlcIndex++) {
  194. int gaLimit = tlcStart + fComponents[tlcIndex].getNumCharacters();
  195. if (gaLimit > startPos) {
  196. break;
  197. }
  198. else {
  199. tlcStart = gaLimit;
  200. }
  201. }
  202. // tlcStart is now the start of the tlc at tlcIndex
  203. int componentCount;
  204. {
  205. boolean split = false;
  206. int compStart = tlcStart;
  207. int lim=tlcIndex;
  208. for (boolean cont=true; cont; lim++) {
  209. int gaLimit = compStart + fComponents[lim].getNumCharacters();
  210. if (cdWsStart > Math.max(compStart, startPos)
  211. && cdWsStart < Math.min(gaLimit, limitPos)) {
  212. split = true;
  213. }
  214. if (gaLimit >= limitPos) {
  215. cont=false;
  216. }
  217. else {
  218. compStart = gaLimit;
  219. }
  220. }
  221. componentCount = lim-tlcIndex;
  222. if (split) {
  223. componentCount++;
  224. }
  225. }
  226. TextLineComponent[] components = new TextLineComponent[componentCount];
  227. int newCompIndex = 0;
  228. int linePos = startPos;
  229. int breakPt = cdWsStart;
  230. int subsetFlag;
  231. if (breakPt == startPos) {
  232. subsetFlag = fIsDirectionLTR? TextLineComponent.LEFT_TO_RIGHT :
  233. TextLineComponent.RIGHT_TO_LEFT;
  234. breakPt = limitPos;
  235. }
  236. else {
  237. subsetFlag = TextLineComponent.UNCHANGED;
  238. }
  239. while (linePos < limitPos) {
  240. int compLength = fComponents[tlcIndex].getNumCharacters();
  241. int tlcLimit = tlcStart + compLength;
  242. int start = Math.max(linePos, tlcStart);
  243. int limit = Math.min(breakPt, tlcLimit);
  244. components[newCompIndex++] = fComponents[tlcIndex].getSubset(
  245. start-tlcStart,
  246. limit-tlcStart,
  247. subsetFlag);
  248. linePos += (limit-start);
  249. if (linePos == breakPt) {
  250. breakPt = limitPos;
  251. subsetFlag = fIsDirectionLTR? TextLineComponent.LEFT_TO_RIGHT :
  252. TextLineComponent.RIGHT_TO_LEFT;
  253. }
  254. if (linePos == tlcLimit) {
  255. tlcIndex++;
  256. tlcStart = tlcLimit;
  257. }
  258. }
  259. return components;
  260. }
  261. private TextLine makeTextLineOnRange(int startPos, int limitPos) {
  262. int[] charsLtoV = null;
  263. byte[] charLevels = null;
  264. if (fBidi != null) {
  265. Bidi lineBidi = fBidi.createLineBidi(startPos, limitPos);
  266. charsLtoV = lineBidi.getLogicalToVisualMap();
  267. charLevels = lineBidi.getLevels();
  268. }
  269. TextLineComponent[] components = makeComponentsOnRange(startPos, limitPos);
  270. return new TextLine(components,
  271. fBaselineOffsets,
  272. fChars,
  273. startPos,
  274. limitPos,
  275. charsLtoV,
  276. charLevels,
  277. fIsDirectionLTR);
  278. }
  279. /**
  280. * Returns the index of the first character which will not fit on
  281. * on a line which begins at <code>start</code> and may be up to
  282. * <code>maxAdvance</code> in graphical width.
  283. *
  284. * @param start the character index at which to start measuring.
  285. * <code>start</code> is an absolute index, not relative to the
  286. * start of the paragraph
  287. * @param maxAdvance the graphical width in which the line must fit
  288. * @return the index after the last character which will fit
  289. * on a line beginning at <code>start</code>, which is not longer
  290. * than <code>maxAdvance</code> in graphical width
  291. */
  292. public int getLineBreakIndex(int start, float maxAdvance) {
  293. return calcLineBreak(start - fStart, maxAdvance) + fStart;
  294. }
  295. /**
  296. * Returns the graphical width of a line beginning at <code>start</code>
  297. * and including characters up to <code>limit</code>.
  298. * <code>start</code> and <code>limit</code> are absolute indices,
  299. * not relative to the start of the paragraph.
  300. *
  301. * @param start the character index at which to start measuring
  302. * @param limit the character index at which to stop measuring
  303. * @return the graphical width of a line beginning at <code>start</code>
  304. * and including characters up to <code>limit</code>
  305. */
  306. public float getAdvanceBetween(int start, int limit) {
  307. TextLine line = makeTextLineOnRange(start - fStart, limit - fStart);
  308. return line.getMetrics().advance;
  309. // could cache line in case getLayout is called with same start, limit
  310. }
  311. /**
  312. * Returns a <code>TextLayout</code> on the given character range.
  313. *
  314. * @param start the index of the first character
  315. * @param limit the index after the last character. Must be greater
  316. * than <code>start</code>
  317. * @return a <code>TextLayout</code> for the characters beginning at
  318. * <code>start</code> up to (but not including) <code>limit</code>
  319. */
  320. public TextLayout getLayout(int start, int limit) {
  321. TextLine textLine = makeTextLineOnRange(start-fStart, limit-fStart);
  322. return new TextLayout(
  323. textLine, fBaseline, fBaselineOffsets, fJustifyRatio);
  324. }
  325. /**
  326. * Updates the <code>TextMeasurer</code> after a single character has
  327. * been inserted
  328. * into the paragraph currently represented by this
  329. * <code>TextMeasurer</code>. After this call, this
  330. * <code>TextMeasurer</code> is equivalent to a new <code>TextMeasurer</code>
  331. * created from the text; however, it will usually be more efficient
  332. * to update an existing <code>TextMeasurer</code> than to create a new one
  333. * from scratch.
  334. *
  335. * @param newParagraph the text of the paragraph after performing
  336. * the insertion. Cannot be null.
  337. * @param insertPos the position in the text where the character was inserted.
  338. * Must not be less than
  339. * the start of <code>newParagraph</code>, and must be less than the
  340. * end of <code>newParagraph</code>.
  341. */
  342. public void insertChar(AttributedCharacterIterator newParagraph, int insertPos) {
  343. fStart = newParagraph.getBeginIndex();
  344. int end = newParagraph.getEndIndex();
  345. if (end - fStart != fChars.length+1) {
  346. initAll(newParagraph);
  347. }
  348. char[] newChars = new char[end-fStart];
  349. int newCharIndex = insertPos - fStart;
  350. System.arraycopy(fChars, 0, newChars, 0, newCharIndex);
  351. char newChar = newParagraph.setIndex(insertPos);
  352. newChars[newCharIndex] = newChar;
  353. System.arraycopy(fChars,
  354. newCharIndex,
  355. newChars,
  356. newCharIndex+1,
  357. end-insertPos-1);
  358. fChars = newChars;
  359. if (fBidi != null || Bidi.requiresBidi(newChar) ||
  360. newParagraph.getAttribute(TextAttribute.BIDI_EMBEDDING) != null) {
  361. fBidi = TextLine.createBidiOnParagraph(newParagraph, fChars);
  362. }
  363. generateComponents(newParagraph);
  364. }
  365. /**
  366. * Updates the <code>TextMeasurer</code> after a single character has
  367. * been deleted
  368. * from the paragraph currently represented by this
  369. * <code>TextMeasurer</code>. After this call, this
  370. * <code>TextMeasurer</code> is equivalent to a new <code>TextMeasurer</code>
  371. * created from the text; however, it will usually be more efficient
  372. * to update an existing <code>TextMeasurer</code> than to create a new one
  373. * from scratch.
  374. *
  375. * @param newParagraph the text of the paragraph after performing
  376. * the deletion. Cannot be null.
  377. * @param deletePos the position in the text where the character was removed.
  378. * Must not be less than
  379. * the start of <code>newParagraph</code>, and must not be greater than the
  380. * end of <code>newParagraph</code>.
  381. */
  382. public void deleteChar(AttributedCharacterIterator newParagraph, int deletePos) {
  383. fStart = newParagraph.getBeginIndex();
  384. int end = newParagraph.getEndIndex();
  385. if (end - fStart != fChars.length-1) {
  386. initAll(newParagraph);
  387. }
  388. char[] newChars = new char[end-fStart];
  389. int changedIndex = deletePos-fStart;
  390. System.arraycopy(fChars, 0, newChars, 0, deletePos-fStart);
  391. System.arraycopy(fChars, changedIndex+1, newChars, changedIndex, end-deletePos);
  392. fChars = newChars;
  393. if (fBidi != null) {
  394. fBidi = TextLine.createBidiOnParagraph(newParagraph, fChars);
  395. }
  396. generateComponents(newParagraph);
  397. }
  398. /**
  399. * NOTE: This method is only for LineBreakMeasurer's use. It is package-
  400. * private because it returns internal data.
  401. */
  402. char[] getChars() {
  403. return fChars;
  404. }
  405. }