PageRenderTime 71ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 1ms

/core/src/main/java/com/metsci/glimpse/core/support/font/SimpleTextLayout.java

http://github.com/metsci/glimpse
Java | 459 lines | 256 code | 67 blank | 136 comment | 28 complexity | 823af25488abb714522337a94c98fb2f MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, CC-BY-SA-3.0
  1. /*
  2. * Copyright (c) 2020, Metron, Inc.
  3. * All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of Metron, Inc. nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  17. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL METRON, INC. BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. package com.metsci.glimpse.core.support.font;
  28. import static java.lang.Math.max;
  29. import static java.lang.Math.min;
  30. import java.awt.Font;
  31. import java.awt.Shape;
  32. import java.awt.font.FontRenderContext;
  33. import java.awt.font.GlyphVector;
  34. import java.awt.geom.Rectangle2D;
  35. import java.text.BreakIterator;
  36. import java.util.ArrayList;
  37. import java.util.Collections;
  38. import java.util.List;
  39. import it.unimi.dsi.fastutil.ints.IntArrayList;
  40. import it.unimi.dsi.fastutil.ints.IntList;
  41. /**
  42. * Performs simple text layout so text can be wrapped in small areas or broken
  43. * on words or newlines. This implementation currently makes the following
  44. * assumptions which are valid for latin text:
  45. *
  46. * <p>
  47. * <ul>
  48. * <li>mapping chars to glyphs is 1:1 in order, (no Unicode surrogate pairs or
  49. * multi-glyph characters)</li>
  50. * <li>left-to-right and top-to-bottom text layout</li>
  51. * </ul>
  52. * </p>
  53. *
  54. * <p>
  55. * All coordinates are in the same space as the text is being drawn (typically
  56. * pixels).
  57. * </p>
  58. *
  59. * @author borkholder
  60. */
  61. public class SimpleTextLayout
  62. {
  63. protected Font font;
  64. protected FontRenderContext frc;
  65. protected BreakIterator breaker;
  66. protected double lineHeight;
  67. /**
  68. * The maximum ascent of any character in the font.
  69. */
  70. protected float ascent;
  71. /**
  72. * The maximum descent of any character in the font.
  73. */
  74. protected float descent;
  75. protected String text;
  76. protected List<TextBoundingBox> lines;
  77. private float lineSpacing;
  78. private boolean breakOnEol;
  79. /**
  80. * @param font
  81. * The font to use for the layout
  82. * @param frc
  83. * The FontRenderContext used to rasterize the font. This will
  84. * help define the character bounds
  85. */
  86. public SimpleTextLayout( Font font, FontRenderContext frc )
  87. {
  88. this( font, frc, BreakIterator.getWordInstance( ) );
  89. }
  90. /**
  91. * @param font
  92. * The font to use for the layout
  93. * @param frc
  94. * The FontRenderContext used to rasterize the font. This will
  95. * help define the character bounds
  96. * @param breaker
  97. * The BreakIterator that is used to break the text (also see
  98. * {@link #forceBreakAfter(int)})
  99. */
  100. public SimpleTextLayout( Font font, FontRenderContext frc, BreakIterator breaker )
  101. {
  102. this.font = font;
  103. this.frc = frc;
  104. this.breaker = breaker;
  105. Rectangle2D rect = font.getMaxCharBounds( frc );
  106. ascent = ( float ) ( rect.getMaxY( ) - rect.getY( ) );
  107. descent = ( float ) -rect.getY( );
  108. lineHeight = font.getSize( );
  109. setLineSpacing( 0 );
  110. setBreakOnEol( true );
  111. }
  112. public double getLineHeight( )
  113. {
  114. return lineHeight;
  115. }
  116. public double getDescent( )
  117. {
  118. return descent;
  119. }
  120. public synchronized double getAscent( )
  121. {
  122. return ascent;
  123. }
  124. public synchronized void setAscent( float ascent )
  125. {
  126. this.ascent = ascent;
  127. }
  128. public void setBreakOnEol( boolean breakOnEol )
  129. {
  130. this.breakOnEol = breakOnEol;
  131. }
  132. /**
  133. * Whether to force a break on the end of line characters (\r \f \n).
  134. */
  135. public boolean getBreakOnEol( )
  136. {
  137. return breakOnEol;
  138. }
  139. public void setLineSpacing( float lineSpacing )
  140. {
  141. this.lineSpacing = lineSpacing;
  142. }
  143. /**
  144. * The spacing between the bottom (descent) of one line of text to the top
  145. * (ascent) of the next line.
  146. */
  147. public float getLineSpacing( )
  148. {
  149. return lineSpacing;
  150. }
  151. /**
  152. * Takes the text and performs the layout. The provided constraints
  153. * determine the top-left position of the text and the maximum width. See
  154. * {@link #numberOfLines()}, {@link #getLine(int)} and
  155. * {@link #getBounds(int)} for the results of the layout.
  156. *
  157. * @param text
  158. * The text to lay out
  159. * @param leftX
  160. * The leftmost X value of any pixel of text
  161. * @param topY
  162. * The topmost Y value of any pixel of text
  163. * @param maxWidth
  164. * The suggested maximum width of any line of the text
  165. */
  166. public void doLayout( String text, float leftX, float topY, float maxWidth )
  167. {
  168. this.text = text;
  169. GlyphVector glyphs = font.createGlyphVector( frc, text );
  170. IntList breaks = getBreaks( glyphs, maxWidth );
  171. if ( breaks.size( ) == 2 )
  172. {
  173. layoutNoBreaks( glyphs, leftX, topY );
  174. return;
  175. }
  176. layout( breaks, glyphs, leftX, topY, maxWidth );
  177. }
  178. /**
  179. * Returns a list of indexes. Each consecutive, overlapping pair of ints is
  180. * a line, inclusive to exclusive.
  181. */
  182. protected IntList getBreaks( GlyphVector glyphs, float maxWidth )
  183. {
  184. IntList breaks = new IntArrayList( );
  185. breaks.add( 0 );
  186. breaker.setText( text );
  187. int lastBreakIdx = 0;
  188. while ( lastBreakIdx < text.length( ) )
  189. {
  190. double currentWidth = 0;
  191. int breakAt = BreakIterator.DONE;
  192. for ( int i = lastBreakIdx; i < text.length( ) && breakAt == BreakIterator.DONE; i++ )
  193. {
  194. if ( forceBreakAfter( i ) )
  195. {
  196. breakAt = i + 1;
  197. }
  198. else
  199. {
  200. Shape l = glyphs.getGlyphLogicalBounds( i );
  201. double width = l.getBounds2D( ).getWidth( );
  202. if ( currentWidth + width > maxWidth )
  203. {
  204. breakAt = breaker.preceding( i );
  205. if ( breakAt == BreakIterator.DONE || breakAt <= lastBreakIdx )
  206. {
  207. breakAt = breaker.following( i );
  208. }
  209. if ( breakAt == BreakIterator.DONE )
  210. {
  211. breakAt = text.length( );
  212. }
  213. }
  214. else
  215. {
  216. currentWidth += width;
  217. }
  218. }
  219. }
  220. if ( breakAt == BreakIterator.DONE )
  221. {
  222. breakAt = text.length( );
  223. }
  224. breaks.add( breakAt );
  225. lastBreakIdx = breakAt;
  226. assert breaks.size( ) <= text.length( );
  227. }
  228. return breaks;
  229. }
  230. protected void layoutNoBreaks( GlyphVector glyphs, float leftX, float topY )
  231. {
  232. Rectangle2D visualBounds = glyphs.getVisualBounds( );
  233. float baseline = ( float ) -visualBounds.getY( );
  234. float width = ( float ) visualBounds.getWidth( );
  235. float height = ( float ) visualBounds.getHeight( );
  236. float minX = ( float ) visualBounds.getMinX( );
  237. float minY = topY - ascent;
  238. lines = Collections.singletonList( new TextBoundingBox( text, baseline, minX, minY, width, height ) );
  239. }
  240. protected void layout( IntList breaks, GlyphVector glyphs, float leftX, float topY, float maxWidth )
  241. {
  242. lines = new ArrayList<TextBoundingBox>( breaks.size( ) - 1 );
  243. double prevMinX = 0;
  244. for ( int i = 1; i < breaks.size( ); i++ )
  245. {
  246. int firstIdx = breaks.getInt( i - 1 );
  247. int lastIdx = breaks.getInt( i ) - 1;
  248. firstIdx = trimLeft( text, firstIdx );
  249. lastIdx = trimRight( text, lastIdx );
  250. if ( lastIdx < firstIdx )
  251. {
  252. continue;
  253. }
  254. double minX = Double.POSITIVE_INFINITY;
  255. double minY = Double.POSITIVE_INFINITY;
  256. double maxX = Double.NEGATIVE_INFINITY;
  257. double maxY = Double.NEGATIVE_INFINITY;
  258. for ( int j = firstIdx; j <= lastIdx; j++ )
  259. {
  260. Rectangle2D b = glyphs.getGlyphLogicalBounds( j ).getBounds2D( );
  261. minX = min( minX, b.getMinX( ) );
  262. minY = min( minY, b.getMinY( ) );
  263. maxX = max( maxX, b.getMaxX( ) );
  264. maxY = max( maxY, b.getMaxY( ) );
  265. }
  266. prevMinX = minX;
  267. double height = maxY - minY;
  268. double width = maxX - minX;
  269. float baseline = topY - ascent;
  270. String line = text.substring( firstIdx, lastIdx + 1 );
  271. TextBoundingBox box = new TextBoundingBox( line, baseline, ( float ) ( minX - prevMinX ), ( float ) minY, ( float ) width, ( float ) height );
  272. lines.add( box );
  273. topY = baseline - getLineSpacing( );
  274. }
  275. }
  276. protected int trimLeft( String text, int index )
  277. {
  278. while ( index < text.length( ) && trimFromLine( text.charAt( index ) ) )
  279. {
  280. index++;
  281. }
  282. return index;
  283. }
  284. protected int trimRight( String text, int index )
  285. {
  286. while ( 0 < index && trimFromLine( text.charAt( index ) ) )
  287. {
  288. index--;
  289. }
  290. return index;
  291. }
  292. protected boolean trimFromLine( char c )
  293. {
  294. return Character.isWhitespace( c );
  295. }
  296. protected boolean forceBreakAfter( int index )
  297. {
  298. char c = text.charAt( index );
  299. return getBreakOnEol( ) && ( c == '\n' || c == '\r' || c == '\f' );
  300. }
  301. public String getSourceText( )
  302. {
  303. return text;
  304. }
  305. public int numberOfLines( )
  306. {
  307. return lines.size( );
  308. }
  309. public TextBoundingBox getLine( int line )
  310. {
  311. return lines.get( line );
  312. }
  313. public List<TextBoundingBox> getLines( )
  314. {
  315. return new ArrayList<TextBoundingBox>( lines );
  316. }
  317. /**
  318. * When drawing using the JOGL {@code TextRenderer}, the {@link #leftX} and
  319. * {@link #baselineY} should be used as the text origin.
  320. *
  321. * @author borkholder
  322. */
  323. public static class TextBoundingBox
  324. {
  325. /**
  326. * The text to display on this line.
  327. */
  328. public final String text;
  329. /**
  330. * The y coordinate for the baseline, this is in the same
  331. * coordinate-system as the provided parameters in
  332. * {@link SimpleTextLayout#doLayout(String, float, float, float)}.
  333. */
  334. public final float baselineY;
  335. /**
  336. * The left-most x coordinate for the text.
  337. */
  338. public final float leftX;
  339. /**
  340. * The maximum descent of any character in this string. If no characters
  341. * in the line have descenders, this will 0 or negative (indicating all
  342. * characters are above the baseline).
  343. */
  344. public final float maxDescent;
  345. /**
  346. * The total width of the string.
  347. */
  348. public final float width;
  349. /**
  350. * The difference between the maximum ascent of any character and the
  351. * lowest descent of any character.
  352. */
  353. public final float maxHeight;
  354. public TextBoundingBox( String text, float baselineY, float leftX, float descent, float width, float height )
  355. {
  356. this.text = text;
  357. this.baselineY = baselineY;
  358. this.leftX = leftX;
  359. this.maxDescent = descent;
  360. this.width = width;
  361. this.maxHeight = height;
  362. }
  363. /**
  364. * Gets the maximum y coordinate of the text bounding box.
  365. */
  366. public float getMaxY( )
  367. {
  368. return baselineY + maxHeight - maxDescent;
  369. }
  370. /**
  371. * Gets the minimum Y coordinate of the text bounding box.
  372. */
  373. public float getMinY( )
  374. {
  375. return baselineY - maxDescent;
  376. }
  377. /**
  378. * Gets the minimum X coordinate of the text bounding box.
  379. */
  380. public float getMinX( )
  381. {
  382. return leftX;
  383. }
  384. /**
  385. * Gets the maximum X coordinate of the text bounding box.
  386. */
  387. public float getMaxX( )
  388. {
  389. return leftX + width;
  390. }
  391. }
  392. }