On Fri, 9 Feb 2024 07:59:47 GMT, Karthik P K <[email protected]> wrote:
>> In the `getHitInfo()` method of PrismTextLayout, RTL node orientation
>> conditions were not considered, hence hit test values such as character
>> index and insertion index values were incorrect.
>>
>> Added checks for RTL orientation of nodes and fixed the issue in
>> `getHitInfo()` to calculate correct hit test values.
>>
>> Added system tests to validate the changes.
>
> Karthik P K has updated the pull request incrementally with one additional
> commit since the last revision:
>
> Review comments
I think we should simplify the `getHitInfo` method in the `TextLayout`
interface.
The code currently seems to be making distinctions where there are none.
`TextFlow`s provide spans, while `Text`s provide a text. `getHitInfo` can take
advantage of this to avoid the `text` and `forTextFlow` parameters altogether.
This will reduce confusion (as there is no distinction) and also avoids cases
that are non-sensical (providing `text` while `forTextFlow` is `true` or vice
versa).
Previous changes to this code (when the parameter `text` was introduced to
`getHitInfo`) should probably be partially undone and simplified with this new
knowledge.
modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextLayout.java
line 213:
> 211: * @param textRunStart Start position of first Text run where hit
> info is requested.
> 212: * @param curRunStart Start position of text run where hit info is
> requested.
> 213: * @param forTextFlow Indicates if the hit info is requested for
> TextFlow or Text node. {@code true} for TextFlow and {@code false} for Text
> node.
I have the impression that we're overcomplicating things here. There is a flag
(`forTextFlow`) for Text/TextFlow, and a String (`text`) for Text/TextFlow, and
there are already `setContent` methods for Text/TextFlow.
I don't see a need for any of these changes to `getHitInfo` at all.
If the content was set with `setContent(TextSpan[] spans)`, then we know it is
a TextFlow (actually we don't care, we have spans which is the difference we're
interested in). This fact can be checked at any time with:
boolean isThisForTextFlow = this.spans != null;
See how the `setContent` methods work; they either set `spans` or they don't.
The rest of the code is already full of alternate paths with `if (spans ==
null) { /* do Text stuff */ } else { /* do TextFlow stuff */ }`
The `text` parameter is also already known, because this is explicitely set for
`Text` nodes because they use `setContent(String text, Object font)`. However,
before using it (specifically for `Text`), make sure that check `spans == null`
as it is filled for `TextFlow` as well at a later point.
So, I think:
- the `text` parameter should never have been added (it wasn't until recently)
- `forTextFlow` flag is unnecessary, just check `spans != null`.
modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java
line 424:
> 422:
> 423: @Override
> 424: public Hit getHitInfo(float x, float y, String text, int
> textRunStart, int curRunStart, boolean forTextFlow) {
This method has become huge, with I think upto 7 levels of nesting. It would
benefit from some splitting.
Suggestions in that area:
- Split it in one that handles RTL and one for LTR **or**
- Split it in one that handles `spans != null` (`TextFlow`) and one that
handles `Text`
You can also reduce the nesting of the first `if/else` by returning early:
if (lineIndex >= getLineCount()) {
charIndex = getCharCount();
insertionIndex = charIndex + 1;
return new Hit(charIndex, insertionIndex, leading); // return
early here
} else { // no need to nest 150+ lines of code
More suggestions inline below
modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java
line 432:
> 430: int ltrIndex = 0;
> 431: int textWidthPrevLine = 0;
> 432: float xHitPos = x;
This variable seems important, yet is only used in the RTL+Text branches.
It seems that this variable can also be easily calculated as:
originalX - (getMirroringWidth() - x) + bounds.getMinX
This calculation can be done in the final RTL+Text branch near the end, no need
to precalculate it IMHO
modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java
line 438:
> 436: if (lineIndex >= getLineCount()) {
> 437: charIndex = getCharCount();
> 438: insertionIndex = charIndex + 1;
By returning early here, you can avoid the `else` and save a lot of nesting.
modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java
line 440:
> 438: insertionIndex = charIndex + 1;
> 439: } else {
> 440: if (isMirrored && (forTextFlow || spans == null)) {
The extra condition here adds nothing of value, the original `if` was correct.
Because `spans == null` means "for Text", and `forTextFlow == true` means "for
TextFlow". Since it always is either a `TextFlow` or a `Text` the `||` always
evaluates to `true` -- unless of course you pass in this flag incorrectly
(again, I think the flag should be removed).
modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java
line 448:
> 446: TextRun run = null;
> 447: //TODO binary search
> 448: if (text == null || spans == null) {
`text` now refers to the `text` parameter here, and not to the original `text`
field -- as discussed earlier, I think you shouldn't be passing in `text` as
argument at all.
Furthermore, I think the original code was also flawed; `text` is only `null`
for a short while, and it is never `null` when `spans == null`. In other
words, this `if` should IMHO be:
Suggestion:
if (spans == null) { // not a TextFlow, must be Text
Use `getText()` function to get the value of the text (it will just use the one
from `Text` set via `setContent(String text, Object font)` or it will calculate
it for `TextFlow` from the spans set via `setContent(TextSpan[] spans)`;
modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java
line 468:
> 466: for (int i = 0; i < runIndex; i++) {
> 467: xHitPos -= runs[i].getWidth();
> 468: }
This code modifies the `x` parameter that was passed in as an argument, making
it harder than necessary to reason about it (I have to talk about "original x"
etc), however, I think this loop, and `runIndex` is not necessary, because
`xHitPos` can just be calculated directly (see comment on `xHitPos`).
modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java
line 567:
> 565: int[] trailing = new int[1];
> 566: if (text != null && spans != null) {
> 567: charIndex = run.getOffsetAtX(x, trailing);
Here also: `text` is no longer referring to the field `text` but to the
argument `text`. I also again think the check is wrong. It should just be:
if (spans != null) { // must be a TextFlow
There are more oddities in the code below (which you didn't change this round
-- see my comments inline:
// !! getText() ***never*** returns `null`, the check is not
needed
if (getText() != null && insertionIndex < getText().length) {
if (!leading) {
BreakIterator charIterator =
BreakIterator.getCharacterInstance();
// before, when `text` was a field, this could never be
null here
if (text != null) {
charIterator.setText(text);
} else {
charIterator.setText(new String(getText()));
}
int next = charIterator.following(insertionIndex);
if (next == BreakIterator.DONE) {
insertionIndex += 1;
} else {
insertionIndex = next;
}
}
} else if (!leading) {
insertionIndex += 1;
}
modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java
line 576:
> 574: int indexOffset;
> 575: if (isMirrored) {
> 576: indexOffset = run.getOffsetAtX(xHitPos,
> trailing);
Here is the only place where `xHitPos` is used again (the RTL + Text case). I
think it can just be calculated (you need to save off the original `x` value,
or even better, don't modify the `x` argument but use a different variable for
the `x` calculation).
modules/javafx.graphics/src/main/java/javafx/scene/text/Text.java line 1035:
> 1033: curRunStart = ((TextRun) runs[runIndex]).getStart();
> 1034: }
> 1035: TextLayout.Hit h = layout.getHitInfo((float)x, (float)y,
> getText(), textRunStart, curRunStart, false);
I think `getText()` as a parameter is not needed here (it is passed at line 260
when it calls `setContent`). Also, I think this should be `getTextInternal()`
-- see the comment in that method.
I also think the `false` is not needed, see comments on the interface.
modules/javafx.graphics/src/main/java/javafx/scene/text/Text.java line 1050:
> 1048:
> 1049: private int findRunIndex(double x, double y, GlyphList[] runs) {
> 1050: int ix = 0;
The naming leaves something to be desired. `ix` is the run index is it not?
Maybe leave it as `runIndex` ?
modules/javafx.graphics/src/main/java/javafx/scene/text/Text.java line 1061:
> 1059: while (ix < lastIndex) {
> 1060: GlyphList r = runs[ix];
> 1061: GlyphList nr = runs[ix + 1];
Suggestion:
GlyphList run = runs[ix];
GlyphList nextRun = runs[ix + 1];
modules/javafx.graphics/src/main/java/javafx/scene/text/Text.java line 1075:
> 1073: double ptY = ptInParent.getY();
> 1074: while (ix < lastIndex) {
> 1075: GlyphList r = runs[ix];
Suggestion:
GlyphList run = runs[ix];
modules/javafx.graphics/src/main/java/javafx/scene/text/TextFlow.java line 202:
> 200: double x = point.getX();
> 201: double y = point.getY();
> 202: TextLayout.Hit h = layout.getHitInfo((float)x, (float)y,
> null, 0, 0, true);
The `null` and `true` here are not needed IMHO (what would even happen when
they conflict, `null` + `false`, or non-`null` + `true`?)
tests/system/src/test/java/test/robot/javafx/scene/RTLTextCharacterIndexTest.java
line 107:
> 105: */
> 106:
> 107: public class RTLTextCharacterIndexTest {
This is a system test, which don't run with the build system. Are you sure this
will work on all platforms? I don't see a specific `Font` used, which means
`X_LEADING_OFFSET` may be incorrect when the platform is supplying a different
font.
tests/system/src/test/java/test/robot/javafx/scene/RTLTextCharacterIndexTest.java
line 121:
> 119:
> 120: final int Y_OFFSET = 30;
> 121: final int X_LEADING_OFFSET = 10;
If they're constant, they should also be `static`, otherwise may want to write
them with lower case.
-------------
Changes requested by jhendrikx (Committer).
PR Review: https://git.openjdk.org/jfx/pull/1323#pullrequestreview-1879325091
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488868375
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488870805
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488873469
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488871928
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488878650
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488876272
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488858423
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488882538
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488881462
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488888035
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488889561
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488889939
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488890087
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488891380
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488892493
PR Review Comment: https://git.openjdk.org/jfx/pull/1323#discussion_r1488894397