@@ -930,8 +930,11 @@ private void shape(TextRun run, char[] chars, GlyphLayout layout) {
930930 }
931931 }
932932
933- private TextLine createLine (int start , int end , int startOffset ) {
933+ private TextLine createLine (int start , int end , int startOffset , float collapsedSpaceWidth ) {
934934 int count = end - start + 1 ;
935+
936+ assert count > 0 : "number of TextRuns in a TextLine cannot be less than one: " + count ;
937+
935938 TextRun [] lineRuns = new TextRun [count ];
936939 if (start < runCount ) {
937940 System .arraycopy (runs , start , lineRuns , 0 , count );
@@ -948,11 +951,43 @@ private TextLine createLine(int start, int end, int startOffset) {
948951 leading = Math .max (leading , run .getLeading ());
949952 length += run .getLength ();
950953 }
954+
955+ width -= collapsedSpaceWidth ;
956+
951957 if (width > layoutWidth ) layoutWidth = width ;
952958 return new TextLine (startOffset , length , lineRuns ,
953959 width , ascent , descent , leading );
954960 }
955961
962+ /**
963+ * Computes the size of the white space trailing a given run.
964+ *
965+ * @param run the run to compute trailing space width for, cannot be {@code null}
966+ * @return the X size of the white space trailing the run
967+ */
968+ private float computeTrailingSpaceWidth (TextRun run ) {
969+ float trailingSpaceWidth = 0 ;
970+ char [] chars = getText ();
971+
972+ /*
973+ * As the loop below exits when encountering a non-white space character,
974+ * testing each trailing glyph in turn for white space is safe, as white
975+ * space is always represented with only a single glyph:
976+ */
977+
978+ for (int i = run .getGlyphCount () - 1 ; i >= 0 ; i --) {
979+ int textOffset = run .getStart () + run .getCharOffset (i );
980+
981+ if (!Character .isWhitespace (chars [textOffset ])) {
982+ break ;
983+ }
984+
985+ trailingSpaceWidth += run .getAdvance (i );
986+ }
987+
988+ return trailingSpaceWidth ;
989+ }
990+
956991 private void reorderLine (TextLine line ) {
957992 TextRun [] runs = line .getRuns ();
958993 int length = runs .length ;
@@ -1059,6 +1094,94 @@ private float getTabAdvance() {
10591094 return tabSize * spaceAdvance ;
10601095 }
10611096
1097+ /*
1098+ * The way JavaFX lays out text:
1099+ *
1100+ * JavaFX distinguishes between soft wraps and hard wraps. Soft wraps
1101+ * occur when a wrap width has been set and the text requires wrapping
1102+ * to stay within the set wrap width. Hard wraps are explicitly part of
1103+ * the text in the form of line feeds (LF) and carriage returns (CR).
1104+ * Hard wrapping considers a singular LF or CR, or the combination of
1105+ * CR+LF (or LF+CR) as a single wrap location. Hard wrapping also occurs
1106+ * between TextSpans when multiple TextSpans were supplied (for wrapping
1107+ * purposes, there is no difference between two TextSpans and a single
1108+ * TextSpan where the text was concatenated with a line break in between).
1109+ *
1110+ * Soft wrapping occurs when a wrap width has been set. This occurs at
1111+ * the first character that does not fit.
1112+ *
1113+ * - If that character is not a white space, the break is set immediately
1114+ * after the first white space encountered before that character
1115+ * - If there is no white space before the preferred break character, the
1116+ * break is done at the first character that does not fit (the wrap
1117+ * then occurs in the middle of a (long) word)
1118+ * - If the preferred break character is white space, and it is followed by
1119+ * more white space, the break is moved to the end of the white space (thus
1120+ * a break in white space always occurs at first non white space character
1121+ * following a white space sequence)
1122+ *
1123+ * White space collapsing:
1124+ *
1125+ * Only white space that is present at soft wrapped locations is collapsed to
1126+ * zero. Any other white space is preserved. This includes white space between
1127+ * words, leading and trailing white space, and white space around hard wrapped
1128+ * locations.
1129+ *
1130+ * Alignment:
1131+ *
1132+ * The alignment calculation only looks at the width of all the significant
1133+ * characters in each line. Significant characters are any non white space
1134+ * characters and any white space that has been preserved (white space that wasn't
1135+ * collapsed due to soft wrapping).
1136+ *
1137+ * Alignment does not take text effects, such as strike through and underline, into
1138+ * account. This means that such effects can appear unaligned. Trailing spaces at a
1139+ * soft wrap location (that are underlined for example), may show the underline go
1140+ * outside the logical bounds of the text.
1141+ *
1142+ * Example, where <SW> indicates a soft wrap location, and <LF> is a line feed:
1143+ *
1144+ * " The quick <SW>brown fox jumps <SW> over the <LF> lazy dog "
1145+ *
1146+ * Would be rendered as (left aligned):
1147+ *
1148+ * " The quick"
1149+ * "brown fox jumps"
1150+ * "over the "
1151+ * " lazy dog "
1152+ *
1153+ * The alignment calculation uses the above bounds indicated by the double
1154+ * quotes, and so right aligned text would look like:
1155+ *
1156+ * " The quick"
1157+ * "brown fox jumps"
1158+ * "over the "
1159+ * " lazy dog "
1160+ *
1161+ * Note that only the white space at the soft wrap locations is collapsed.
1162+ * In all other locations the space was preserved (the space between words
1163+ * where no soft wrap occurred, the leading and trailing space, and the
1164+ * space around the hard wrapped location).
1165+ *
1166+ * Text effects have no effect on the alignment, and so with underlining on
1167+ * the right aligned text would look like:
1168+ *
1169+ * "___The___quick_" (one collapsed space becomes visible here)
1170+ * "brown_fox_jumps__" (two collapsed spaces become visible here)
1171+ * "over_the_"
1172+ * "_lazy_dog___"
1173+ *
1174+ * Note that text alignment has not changed at all, but the bounds are exceeded
1175+ * in some locations to allow for the underline. Controls displaying such texts
1176+ * will likely clip the underlined parts exceeding the bounds.
1177+ *
1178+ * Users wishing to mitigate some of these perhaps surprising results can ensure
1179+ * they use trimmed texts, and avoid the use of line breaks, or at least ensure
1180+ * that line breaks are not preceded or succeeded by white space (activating
1181+ * line wrapping is not equivalent to collapsing any consecutive white space
1182+ * no matter where it occurs).
1183+ */
1184+
10621185 private void layout () {
10631186 /* Try the cache */
10641187 initCache ();
@@ -1126,17 +1249,22 @@ private void layout() {
11261249 /* Find offset of the first character that does not fit on the line */
11271250 int hitOffset = run .getStart () + run .getWrapIndex (wrapWidth - lineWidth );
11281251
1129- /* Only keep whitespaces (not tabs) in the current run to avoid
1252+ /*
1253+ * Only keep white spaces (not tabs) in the current run to avoid
11301254 * dealing with unshaped runs.
1255+ *
1256+ * If the run is a tab, the run will be always of length 1 (see
1257+ * buildRuns()). As there is no "next" character that can be selected
1258+ * as the wrap index in this run, the white space skipping logic
1259+ * below won't skip tabs.
11311260 */
1261+
11321262 int offset = hitOffset ;
11331263 int runEnd = run .getEnd ();
1134- while (offset + 1 < runEnd && chars [offset ] == ' ' ) {
1264+
1265+ // Don't take white space into account at the preferred wrap index:
1266+ while (offset + 1 < runEnd && Character .isWhitespace (chars [offset ])) {
11351267 offset ++;
1136- /* Preserve behaviour: only keep one white space in the line
1137- * before wrapping. Needed API to allow change.
1138- */
1139- break ;
11401268 }
11411269
11421270 /* Find the break opportunity */
@@ -1233,7 +1361,7 @@ private void layout() {
12331361
12341362 lineWidth += runWidth ;
12351363 if (run .isBreak ()) {
1236- TextLine line = createLine (startIndex , i , startOffset );
1364+ TextLine line = createLine (startIndex , i , startOffset , computeTrailingSpaceWidth ( runs [ i ]) );
12371365 linesList .add (line );
12381366 startIndex = i + 1 ;
12391367 startOffset += line .getLength ();
@@ -1242,11 +1370,11 @@ private void layout() {
12421370 }
12431371 if (layout != null ) layout .dispose ();
12441372
1245- linesList .add (createLine (startIndex , runCount - 1 , startOffset ));
1373+ linesList .add (createLine (startIndex , runCount - 1 , startOffset , 0 ));
12461374 lines = new TextLine [linesList .size ()];
12471375 linesList .toArray (lines );
12481376
1249- float fullWidth = Math . max ( wrapWidth , layoutWidth );
1377+ float fullWidth = wrapWidth > 0 ? wrapWidth : layoutWidth ; // layoutWidth = widest line, wrapWidth is user set
12501378 float lineY = 0 ;
12511379 float align ;
12521380 if (isMirrored ()) {
@@ -1263,7 +1391,8 @@ private void layout() {
12631391 RectBounds bounds = line .getBounds ();
12641392
12651393 /* Center and right alignment */
1266- float lineX = (fullWidth - bounds .getWidth ()) * align ;
1394+ float unusedWidth = fullWidth - bounds .getWidth ();
1395+ float lineX = unusedWidth * align ;
12671396 line .setAlignment (lineX );
12681397
12691398 /* Justify */
@@ -1281,7 +1410,7 @@ private void layout() {
12811410 if (hitChar && chars [j ] == ' ' ) wsCount ++;
12821411 }
12831412 if (wsCount != 0 ) {
1284- float inc = ( fullWidth - bounds . getWidth ()) / wsCount ;
1413+ float inc = unusedWidth / wsCount ;
12851414 done :
12861415 for (int j = 0 ; j < lineRunCount ; j ++) {
12871416 TextRun textRun = lineRuns [j ];
0 commit comments