Skip to content

Commit 398f104

Browse files
committed
8314215: Trailing Spaces before Line Breaks Affect the Center Alignment of Text
Reviewed-by: kpk, angorya
1 parent c23ac74 commit 398f104

File tree

7 files changed

+780
-249
lines changed

7 files changed

+780
-249
lines changed

modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextLine.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2014, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -27,9 +27,12 @@
2727

2828
import com.sun.javafx.geom.RectBounds;
2929

30+
/**
31+
* Represents a full line of text that is all rendered on a single line.
32+
*/
3033
public interface TextLine {
3134
/**
32-
* Returns the list of GlyphList in the line. The list is visually orderded.
35+
* Returns the list of GlyphList in the line. The list is visually ordered.
3336
*/
3437
public GlyphList[] getRuns();
3538

modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextSpan.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -27,6 +27,13 @@
2727

2828
import com.sun.javafx.geom.RectBounds;
2929

30+
/**
31+
* Represents a sequence of characters all using the same font, or
32+
* an embedded object if no font is supplied.
33+
* <p>
34+
* A text span can contain line breaks if the text should span multiple
35+
* lines.
36+
*/
3037
public interface TextSpan {
3138
/**
3239
* The text for the span, can be empty but not null.

modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java

Lines changed: 141 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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];

modules/javafx.graphics/src/main/java/com/sun/javafx/text/TextLine.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -28,11 +28,13 @@
2828
import com.sun.javafx.geom.RectBounds;
2929

3030
public class TextLine implements com.sun.javafx.scene.text.TextLine {
31-
TextRun[] runs;
32-
RectBounds bounds;
33-
float lsb, rsb, leading;
34-
int start;
35-
int length;
31+
private final TextRun[] runs;
32+
private final RectBounds bounds;
33+
private final int start;
34+
private final int length;
35+
private final float leading;
36+
37+
private float lsb, rsb;
3638

3739
public TextLine(int start, int length, TextRun[] runs,
3840
float width, float ascent, float descent, float leading) {

modules/javafx.graphics/src/main/java/com/sun/javafx/text/TextRun.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -343,6 +343,19 @@ public int getWrapIndex(float width) {
343343
}
344344

345345
public float getAdvance(int glyphIndex) {
346+
347+
/*
348+
* When positions is null it means that the TextRun only contains
349+
* a line break, assuming that the class is used correctly ("shape"
350+
* must be called before calling this method, unless the class user is
351+
* sure that the run is empty). This class could benefit from better
352+
* encapsulation to make it easier to reason about.
353+
*/
354+
355+
if (positions == null) {
356+
return 0;
357+
}
358+
346359
if ((flags & FLAGS_COMPACT) != 0) {
347360
return positions[start + glyphIndex];
348361
} else {

0 commit comments

Comments
 (0)