diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextLayout.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextLayout.java
index 5b561fc1fb1..74fdaa8e6d1 100644
--- a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextLayout.java
+++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextLayout.java
@@ -29,6 +29,8 @@
import com.sun.javafx.geom.BaseBounds;
import com.sun.javafx.geom.Shape;
+import java.util.Objects;
+
public interface TextLayout {
/* Internal flags Flags */
@@ -91,6 +93,28 @@ public Hit(int charIndex, int insertionIndex, boolean leading) {
public int getCharIndex() { return charIndex; }
public int getInsertionIndex() { return insertionIndex; }
public boolean isLeading() { return leading; }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(charIndex, insertionIndex, leading);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Hit other = (Hit) obj;
+ return charIndex == other.charIndex && insertionIndex == other.insertionIndex && leading == other.leading;
+ }
+
+ @Override
+ public String toString() {
+ return "Hit[charIndex=" + charIndex + ", insertionIndex=" + insertionIndex + ", leading=" + leading + "]";
+ }
}
/**
@@ -205,14 +229,9 @@ public Hit(int charIndex, int insertionIndex, boolean leading) {
*
* @param x x coordinate value.
* @param y y coordinate value.
- * @param text text for which HitInfo needs to be calculated.
- * It is expected to be null in the case of {@link javafx.scene.text.TextFlow}
- * and non-null in the case of {@link javafx.scene.text.Text}
- * @param textRunStart Text run start position.
- * @param curRunStart starting position of text run where hit info is requested.
* @return returns a {@link Hit} object containing character index, insertion index and position of cursor on the character.
*/
- public Hit getHitInfo(float x, float y, String text, int textRunStart, int curRunStart);
+ public Hit getHitInfo(float x, float y);
public PathElement[] getCaretShape(int offset, boolean isLeading,
float x, float y);
diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java
index c851167428e..7d67816821c 100644
--- a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java
+++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayout.java
@@ -421,90 +421,44 @@ public PathElement[] getCaretShape(int offset, boolean isLeading,
}
@Override
- public Hit getHitInfo(float x, float y, String text, int textRunStart, int curRunStart) {
+ public Hit getHitInfo(float x, float y) {
int charIndex = -1;
int insertionIndex = -1;
boolean leading = false;
- int relIndex = 0;
- int textWidthPrevLine = 0;
ensureLayout();
- int lineIndex = getLineIndex(y, text, curRunStart);
+ int lineIndex = getLineIndex(y);
if (lineIndex >= getLineCount()) {
charIndex = getCharCount();
insertionIndex = charIndex + 1;
} else {
- if (isMirrored()) {
- x = getMirroringWidth() - x;
- }
TextLine line = lines[lineIndex];
TextRun[] runs = line.getRuns();
RectBounds bounds = line.getBounds();
TextRun run = null;
x -= bounds.getMinX();
- //TODO binary search
- if (text == null || spans == null) {
- for (int i = 0; i < runs.length; i++) {
- run = runs[i];
- if (x < run.getWidth()) break;
- if (i + 1 < runs.length) {
- if (runs[i + 1].isLinebreak()) break;
- x -= run.getWidth();
- }
- }
- } else {
- for (int i = 0; i < lineIndex; i++) {
- for (TextRun r: lines[i].runs) {
- if (r.getTextSpan() != null && r.getStart() >= textRunStart && r.getTextSpan().getText().equals(text)) {
- textWidthPrevLine += r.getLength();
- }
- }
+ for (int i = 0; i < runs.length; i++) {
+ run = runs[i];
+ if (x < run.getWidth()) {
+ break;
}
- int prevNodeLength = 0;
- boolean isPrevNodeExcluded = false;
- for (TextRun r: runs) {
- if (!r.getTextSpan().getText().equals(text) || (r.getStart() < textRunStart && r.getTextSpan().getText().equals(text))) {
- prevNodeLength += r.getWidth();
- continue;
- }
- if (r.getTextSpan() != null && r.getTextSpan().getText().equals(text)) {
- BaseBounds textBounds = new BoxBounds();
- getBounds(r.getTextSpan(), textBounds);
- if (textBounds.getMinX() == 0 && !isPrevNodeExcluded) {
- x -= prevNodeLength;
- isPrevNodeExcluded = true;
- }
- if (x > r.getWidth()) {
- x -= r.getWidth();
- relIndex += r.getLength();
- continue;
- }
- run = r;
+ if (i + 1 < runs.length) {
+ if (runs[i + 1].isLinebreak()) {
break;
}
+ x -= run.getWidth();
}
}
-
if (run != null) {
int[] trailing = new int[1];
- if (text != null && spans != null) {
- charIndex = run.getOffsetAtX(x, trailing);
- charIndex += textWidthPrevLine;
- charIndex += relIndex;
- } else {
- charIndex = run.getStart() + run.getOffsetAtX(x, trailing);
- }
+ charIndex = run.getStart() + run.getOffsetAtX(x, trailing);
leading = (trailing[0] == 0);
insertionIndex = charIndex;
if (getText() != null && insertionIndex < getText().length) {
if (!leading) {
BreakIterator charIterator = BreakIterator.getCharacterInstance();
- if (text != null) {
- charIterator.setText(text);
- } else {
- charIterator.setText(new String(getText()));
- }
+ charIterator.setText(new String(getText()));
int next = charIterator.following(insertionIndex);
if (next == BreakIterator.DONE) {
insertionIndex += 1;
@@ -749,30 +703,17 @@ public boolean setTabSize(int spaces) {
* *
**************************************************************************/
- private int getLineIndex(float y, String text, int runStart) {
+ private int getLineIndex(float y) {
int index = 0;
float bottom = 0;
- /* Initializing textFound as true when text is null
- * because when this function is called for TextFlow text parameter will be null */
- boolean textFound = (text == null);
int lineCount = getLineCount();
while (index < lineCount) {
- if (!textFound) {
- for (TextRun r : lines[index].runs) {
- if (r.getTextSpan() == null || (r.getStart() == runStart && r.getTextSpan().getText().equals(text))) {
- /* Span will present only for Rich Text.
- * Hence making textFound as true */
- textFound = true;
- break;
- }
- }
- }
bottom += lines[index].getBounds().getHeight() + spacing;
if (index + 1 == lineCount) {
bottom -= lines[index].getLeading();
}
- if (bottom > y && textFound) {
+ if (bottom > y) {
break;
}
index++;
diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/text/Text.java b/modules/javafx.graphics/src/main/java/javafx/scene/text/Text.java
index d557a5e8878..288c01c9afe 100644
--- a/modules/javafx.graphics/src/main/java/javafx/scene/text/Text.java
+++ b/modules/javafx.graphics/src/main/java/javafx/scene/text/Text.java
@@ -1021,27 +1021,33 @@ public final BooleanProperty caretBiasProperty() {
public final HitInfo hitTest(Point2D point) {
if (point == null) return null;
TextLayout layout = getTextLayout();
+
double x = point.getX() - getX();
double y = point.getY() - getY() + getYRendering();
- GlyphList[] runs = getRuns();
- int runIndex = 0;
- if (runs.length != 0) {
- double ptY = localToParent(x, y).getY();
- while (runIndex < runs.length - 1) {
- if (ptY > runs[runIndex].getLocation().y && ptY < runs[runIndex + 1].getLocation().y) {
- break;
- }
- runIndex++;
- }
+
+ int textRunStart = findFirstRunStart();
+
+ double px = x;
+ double py = y;
+
+ if (isSpan()) {
+ Point2D pPoint = localToParent(point);
+ px = pPoint.getX();
+ py = pPoint.getY();
}
- int textRunStart = 0;
- int curRunStart = 0;
- if (runs.length != 0) {
- textRunStart = ((TextRun) runs[0]).getStart();
- curRunStart = ((TextRun) runs[runIndex]).getStart();
+ TextLayout.Hit h = layout.getHitInfo((float)px, (float)py);
+ return new HitInfo(h.getCharIndex() - textRunStart, h.getInsertionIndex() - textRunStart, h.isLeading());
+ }
+
+ private int findFirstRunStart() {
+ int start = Integer.MAX_VALUE;
+ for (GlyphList r: getRuns()) {
+ int runStart = ((TextRun) r).getStart();
+ if (runStart < start) {
+ start = runStart;
+ }
}
- TextLayout.Hit h = layout.getHitInfo((float)x, (float)y, getText(), textRunStart, curRunStart);
- return new HitInfo(h.getCharIndex(), h.getInsertionIndex(), h.isLeading());
+ return start;
}
private PathElement[] getRange(int start, int end, int type) {
diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/text/TextFlow.java b/modules/javafx.graphics/src/main/java/javafx/scene/text/TextFlow.java
index 36a92297d92..aba73dd0d3c 100644
--- a/modules/javafx.graphics/src/main/java/javafx/scene/text/TextFlow.java
+++ b/modules/javafx.graphics/src/main/java/javafx/scene/text/TextFlow.java
@@ -199,7 +199,7 @@ public final HitInfo hitTest(javafx.geometry.Point2D point) {
TextLayout layout = getTextLayout();
double x = point.getX();
double y = point.getY();
- TextLayout.Hit h = layout.getHitInfo((float)x, (float)y, null, 0, 0);
+ TextLayout.Hit h = layout.getHitInfo((float)x, (float)y);
return new HitInfo(h.getCharIndex(), h.getInsertionIndex(), h.isLeading());
} else {
return null;
diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubTextLayout.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubTextLayout.java
index e079fa40c04..4c58daa085d 100644
--- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubTextLayout.java
+++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubTextLayout.java
@@ -158,7 +158,7 @@ public Shape getShape(int type, TextSpan filter) {
}
@Override
- public Hit getHitInfo(float x, float y, String text, int textRunStart, int curRunStart) {
+ public Hit getHitInfo(float x, float y) {
// TODO this probably needs to be entirely rewritten...
if (getText() == null) {
return new Hit(0, -1, true);
diff --git a/tests/system/src/test/.classpath b/tests/system/src/test/.classpath
index 6760dad16e8..2ced868591c 100644
--- a/tests/system/src/test/.classpath
+++ b/tests/system/src/test/.classpath
@@ -14,7 +14,7 @@
-
+
diff --git a/tests/system/src/test/addExports b/tests/system/src/test/addExports
index a4da975b9c1..8ed570ad6a2 100644
--- a/tests/system/src/test/addExports
+++ b/tests/system/src/test/addExports
@@ -23,6 +23,8 @@
--add-exports javafx.graphics/com.sun.javafx.image=ALL-UNNAMED
--add-exports javafx.graphics/com.sun.javafx.sg.prism=ALL-UNNAMED
--add-exports javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED
+--add-exports javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED
+--add-exports javafx.graphics/com.sun.javafx.text=ALL-UNNAMED
--add-exports javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED
--add-exports javafx.graphics/com.sun.prism.impl=ALL-UNNAMED
#
diff --git a/tests/system/src/test/java/test/com/sun/javafx/text/TextHitInfoTest.java b/tests/system/src/test/java/test/com/sun/javafx/text/TextHitInfoTest.java
new file mode 100644
index 00000000000..6ab2aae3426
--- /dev/null
+++ b/tests/system/src/test/java/test/com/sun/javafx/text/TextHitInfoTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package test.com.sun.javafx.text;
+
+import static org.junit.Assume.assumeTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+import com.sun.javafx.font.PGFont;
+import com.sun.javafx.geom.RectBounds;
+import com.sun.javafx.scene.text.FontHelper;
+import com.sun.javafx.scene.text.TextLayout.Hit;
+import com.sun.javafx.scene.text.TextSpan;
+import com.sun.javafx.text.PrismTextLayout;
+
+import javafx.scene.text.Font;
+
+public class TextHitInfoTest {
+ private final PrismTextLayout layout = new PrismTextLayout();
+ private final PGFont arialFont = (PGFont) FontHelper.getNativeFont(Font.font("Arial", 12));
+
+ record TestSpan(String text, Object font) implements TextSpan {
+ @Override
+ public String getText() {
+ return text;
+ }
+
+ @Override
+ public Object getFont() {
+ return font;
+ }
+
+ @Override
+ public RectBounds getBounds() {
+ return null;
+ }
+ }
+
+ @Test
+ void getHitInfoTest() {
+ assumeArialFontAvailable();
+
+ /*
+ * Empty line:
+ */
+
+ layout.setContent("", arialFont);
+
+ // Checks that hits above the line results in first character:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(0, -30));
+
+ // Checks before start of line:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(-50, 0));
+
+ // Checks position of empty string:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(0, 0));
+
+ // Checks past end of line:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(250, 0));
+
+ // Checks that hits below the line results in last character + 1:
+ assertEquals(new Hit(0, 1, false), layout.getHitInfo(0, 30));
+
+ /*
+ * Single line:
+ */
+
+ layout.setContent("The quick brown fox jumps over the lazy dog", arialFont);
+
+ // Checks that hits above the line results in first character:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(0, -30));
+
+ // Checks before start of line:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(-50, 0));
+
+ // Checks positions of a few characters:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(0, 0)); // Start of "T"
+ assertEquals(new Hit(0, 1, false), layout.getHitInfo(5, 0)); // Past halfway of "T"
+ assertEquals(new Hit(1, 1, true), layout.getHitInfo(10, 0)); // Start of "h"
+
+ // Checks past end of line:
+ assertEquals(new Hit(42, 43, false), layout.getHitInfo(250, 0));
+
+ // Checks that hits below the line results in last character + 1:
+ assertEquals(new Hit(43, 44, false), layout.getHitInfo(0, 30));
+
+ /*
+ * Multi line:
+ */
+
+ layout.setContent("The\nquick\nbrown\nfox\n", arialFont);
+
+ // Checks that hits above the first line results in first character:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(0, -30));
+
+ // Checks before start of first line:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(-50, 0));
+
+ // Checks positions of a few characters on first line:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(0, 0)); // Start of "T"
+ assertEquals(new Hit(0, 1, false), layout.getHitInfo(5, 0)); // Halfway past "T"
+ assertEquals(new Hit(1, 1, true), layout.getHitInfo(10, 0)); // Start of "h"
+
+ // Checks past end of first line:
+ assertEquals(new Hit(2, 3, false), layout.getHitInfo(250, 0));
+
+ // Checks before start of second line:
+ assertEquals(new Hit(4, 4, true), layout.getHitInfo(-50, 15));
+
+ // Check second line:
+ assertEquals(new Hit(4, 4, true), layout.getHitInfo(0, 15)); // Start of "q"
+
+ // Checks past end of second line:
+ assertEquals(new Hit(8, 9, false), layout.getHitInfo(250, 15));
+
+ /*
+ * Test with two spans:
+ */
+
+ layout.setContent(new TestSpan[] {new TestSpan("Two", arialFont), new TestSpan("Spans", arialFont)});
+
+ // Checks that hits above the line results in first character:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(0, -30));
+
+ // Checks before start of line:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(-50, 0));
+
+ // Checks positions of a few characters:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(0, 0)); // Start of "T"
+ assertEquals(new Hit(0, 1, false), layout.getHitInfo(5, 0)); // Past halfway of "T"
+ assertEquals(new Hit(1, 1, true), layout.getHitInfo(10, 0)); // Start of "w"
+
+ assertEquals(new Hit(7, 8, false), layout.getHitInfo(60, 0)); // Past halfway of "s"
+
+ // Checks past end of line:
+ assertEquals(new Hit(7, 8, false), layout.getHitInfo(250, 0));
+
+ // Checks that hits below the line results in last character + 1:
+ assertEquals(new Hit(8, 9, false), layout.getHitInfo(0, 30));
+
+ /*
+ * Test with zero spans:
+ */
+
+ layout.setContent(new TestSpan[] {});
+
+ // Checks that hits above the line results in first character:
+ assertEquals(new Hit(0, 0, true), layout.getHitInfo(0, -30));
+
+ // Checks before start of line:
+ assertEquals(new Hit(0, 1, false), layout.getHitInfo(-50, 0));
+
+ // Checks positions of center:
+ assertEquals(new Hit(0, 1, false), layout.getHitInfo(0, 0)); // Start of "T"
+
+ // Checks past end of line:
+ assertEquals(new Hit(0, 1, false), layout.getHitInfo(250, 0));
+
+ // Checks that hits below the line results in last character + 1:
+ assertEquals(new Hit(0, 1, false), layout.getHitInfo(0, 30));
+
+ }
+
+ private void assumeArialFontAvailable() {
+ assumeTrue("Arial font missing", arialFont.getName().equals("Arial"));
+ }
+}
diff --git a/tests/system/src/test/java/test/robot/javafx/scene/RTLTextCharacterIndexTest.java b/tests/system/src/test/java/test/robot/javafx/scene/RTLTextCharacterIndexTest.java
new file mode 100644
index 00000000000..22428c52dc4
--- /dev/null
+++ b/tests/system/src/test/java/test/robot/javafx/scene/RTLTextCharacterIndexTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package test.robot.javafx.scene;
+
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.geometry.NodeOrientation;
+import javafx.geometry.Point2D;
+import javafx.geometry.Point3D;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.PickResult;
+import javafx.scene.layout.VBox;
+import javafx.scene.robot.Robot;
+import javafx.scene.text.Font;
+import javafx.scene.text.HitInfo;
+import javafx.scene.text.Text;
+import javafx.stage.Stage;
+import javafx.stage.StageStyle;
+import javafx.stage.Window;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import test.util.Util;
+
+/*
+ * Test for verifying character index of Text nodes in RTL orientation.
+ *
+ * There are 6 tests in this file.
+ * Here, the scene node orientation is set to RTL for all the tests.
+ * Steps for testTextInfoForRTLEnglishText()
+ * 1. Create a Text node and add it to the scene using a VBox.
+ * 2. Add only english text to the Text node.
+ * 3. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 4. Character index should change from highest value to lowest
+ * as expected.
+ *
+ * Steps for testTextInfoForRTLArabicText()
+ * 1. Create a Text node and add it to the scene using a VBox.
+ * 2. Add only arabic text to the Text node.
+ * 3. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 3. Character index should increment as expected since it is RTL text.
+ *
+ * Steps for testTextInfoForRTLEnglishArabicText()
+ * 1. Create a Text node and add it to the scene using a VBox.
+ * 2. Add both english and arabic text to the Text node.
+ * 3. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 4. Character index should change in decreasing order for english text
+ * and in increasing order for arabic text.
+ *
+ * Steps for testTextInfoForMultiLineRTLEnglishText()
+ * 1. Create a Text node and add it to the scene using a VBox.
+ * 2. Add two lines of only english text to the Text node.
+ * 3. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 4. Character index should change in decreasing order as expected.
+ *
+ * Steps for testTextInfoForMultiLineRTLEnglishArabicText()
+ * 1. Create a Text node and add it to the scene using a VBox.
+ * 2. Add two lines of both english and arabic text to the Text node.
+ * 3. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 4. Character index should change in decreasing order for english text
+ * and increasing order for arabic text.
+ *
+ * Steps for testTextInfoForMultiLineRTLArabicText()
+ * 1. Create a Text node and add it to the scene using a VBox.
+ * 2. Add two lines of only arabic text to the Text node.
+ * 3. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 4. Character index should change in increasing order as expected.
+ */
+
+public class RTLTextCharacterIndexTest {
+ static CountDownLatch startupLatch = new CountDownLatch(1);
+ static Random random;
+ static Robot robot;
+ static Text text;
+ static VBox vBox;
+
+ static volatile Stage stage;
+ static volatile Scene scene;
+
+ static final int WIDTH = 500;
+ static final int HEIGHT = 200;
+
+ static final int Y_OFFSET = 30;
+ static final int X_LEADING_OFFSET = 10;
+
+ boolean isLeading;
+ boolean textFlowIsLeading;
+ int charIndex;
+ int insertionIndex;
+ int textFlowCharIndex;
+ int textFlowInsertionIndex;
+
+ private void mouseClick(double x, double y) {
+ Util.runAndWait(() -> {
+ Window w = scene.getWindow();
+ robot.mouseMove(w.getX() + scene.getX() + x,
+ w.getY() + scene.getY() + y);
+ robot.mouseClick(MouseButton.PRIMARY);
+ });
+ }
+
+ private void moveMouseOverText(double x, double y) throws Exception {
+ mouseClick(text.getLayoutX() + x,
+ text.getLayoutY() / 2 + y);
+ }
+
+ private void addRTLEnglishText() {
+ Util.runAndWait(() -> {
+ text.setText("This is text");
+ text.setFont(new Font(48));
+ vBox.getChildren().setAll(text);
+ });
+ }
+
+ private void addRTLArabicText() {
+ Util.runAndWait(() -> {
+ text.setText("شسيبلاتنم");
+ text.setFont(new Font(48));
+ vBox.getChildren().setAll(text);
+ });
+ }
+
+ private void addRTLEnglishArabicText() {
+ Util.runAndWait(() -> {
+ text.setText("Arabic:شسيبلاتنم");
+ text.setFont(new Font(48));
+ vBox.getChildren().setAll(text);
+ });
+ }
+
+ private void addMultiLineRTLEnglishText() {
+ Util.runAndWait(() -> {
+ text.setText("This is text\nThis is text");
+ text.setFont(new Font(48));
+ vBox.getChildren().setAll(text);
+ });
+ }
+
+ private void addMultiLineRTLEnglishArabicText() {
+ Util.runAndWait(() -> {
+ text.setText("Arabic:شسيبلاتنم\nArabic:شسيبلاتنم");
+ text.setFont(new Font(48));
+ vBox.getChildren().setAll(text);
+ });
+ }
+
+ private void addMultiLineRTLArabicText() {
+ Util.runAndWait(() -> {
+ text.setText("شسيبلاتنم شسيبلاتنم\nشسيبلاتنم شسيبلاتنم");
+ text.setFont(new Font(48));
+ vBox.getChildren().setAll(text);
+ });
+ }
+
+ @Test
+ public void testTextInfoForRTLEnglishText() throws Exception {
+ addRTLEnglishText();
+ Util.waitForIdle(scene);
+
+ int textLength = text.getText().length();
+
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverText(x, 0);
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < textLength);
+ x -= step();
+ }
+ }
+
+ @Test
+ public void testTextInfoForRTLArabicText() throws Exception {
+ addRTLArabicText();
+ Util.waitForIdle(scene);
+
+ int textLength = text.getText().length();
+
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverText(x, 0);
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < textLength);
+ x -= step();
+ }
+ }
+
+ @Test
+ public void testTextInfoForRTLEnglishArabicText() throws Exception {
+ addRTLEnglishArabicText();
+ Util.waitForIdle(scene);
+
+ int textLength = text.getText().length();
+
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverText(x, 0);
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < textLength);
+ x -= step();
+ }
+ }
+
+ @Test
+ public void testTextInfoForMultiLineRTLEnglishText() throws Exception {
+ addMultiLineRTLEnglishText();
+ Util.waitForIdle(scene);
+
+ int textLength = text.getText().length();
+
+ for (int y = 0; y < 2; y++) {
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverText(x, (Y_OFFSET * (y * 2)));
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < textLength);
+ x -= step();
+ }
+ }
+ }
+
+ @Test
+ public void testTextInfoForMultiLineRTLEnglishArabicText() throws Exception {
+ addMultiLineRTLEnglishArabicText();
+ Util.waitForIdle(scene);
+
+ int textLength = text.getText().length();
+
+ for (int y = 0; y < 2; y++) {
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverText(x, (Y_OFFSET * (y * 2)));
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < textLength);
+ x -= step();
+ }
+ }
+ }
+
+ @Test
+ public void testTextInfoForMultiLineRTLArabicText() throws Exception {
+ addMultiLineRTLArabicText();
+ Util.waitForIdle(scene);
+
+ int textLength = text.getText().length();
+
+ for (int y = 0; y < 2; y++) {
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverText(x, (Y_OFFSET * (y * 2)));
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < textLength);
+ x -= step();
+ }
+ }
+ }
+
+ private void handleTextMouseEvent(MouseEvent event) {
+ PickResult pick = event.getPickResult();
+ Node n = pick.getIntersectedNode();
+
+ if (n != null && n instanceof Text t) {
+ Point3D p3 = pick.getIntersectedPoint();
+ Point2D p = new Point2D(p3.getX(), p3.getY());
+ HitInfo hitInfo = t.hitTest(p);
+
+ isLeading = hitInfo.isLeading();
+ charIndex = hitInfo.getCharIndex();
+ insertionIndex = hitInfo.getInsertionIndex();
+ }
+ }
+
+ private double step() {
+ return 1.0 + random.nextDouble() * 8.0;
+ }
+
+ @AfterEach
+ public void resetUI() {
+ Platform.runLater(() -> {
+ text.removeEventHandler(MouseEvent.MOUSE_PRESSED, this::handleTextMouseEvent);
+ });
+ }
+
+ @BeforeEach
+ public void setupUI() {
+ Platform.runLater(() -> {
+ text.addEventHandler(MouseEvent.MOUSE_PRESSED, this::handleTextMouseEvent);
+ });
+ }
+
+ @BeforeAll
+ public static void initFX() {
+ long seed = new Random().nextLong();
+ System.out.println("seed=" + seed);
+ random = new Random(seed);
+
+ Util.launch(startupLatch, TestApp.class);
+ }
+
+ @AfterAll
+ public static void exit() {
+ Util.shutdown(stage);
+ }
+
+ public static class TestApp extends Application {
+ @Override
+ public void start(Stage primaryStage) {
+ robot = new Robot();
+ stage = primaryStage;
+
+ text = new Text();
+ vBox = new VBox();
+
+ scene = new Scene(vBox, WIDTH, HEIGHT);
+ scene.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);
+ stage.setScene(scene);
+ stage.initStyle(StageStyle.UNDECORATED);
+ stage.setOnShown(event -> Platform.runLater(startupLatch::countDown));
+ stage.setAlwaysOnTop(true);
+ stage.show();
+ }
+ }
+}
diff --git a/tests/system/src/test/java/test/robot/javafx/scene/RTLTextFlowCharacterIndexTest.java b/tests/system/src/test/java/test/robot/javafx/scene/RTLTextFlowCharacterIndexTest.java
new file mode 100644
index 00000000000..51bb0df668c
--- /dev/null
+++ b/tests/system/src/test/java/test/robot/javafx/scene/RTLTextFlowCharacterIndexTest.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package test.robot.javafx.scene;
+
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.geometry.NodeOrientation;
+import javafx.geometry.Point2D;
+import javafx.geometry.Point3D;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.PickResult;
+import javafx.scene.layout.VBox;
+import javafx.scene.robot.Robot;
+import javafx.scene.text.Font;
+import javafx.scene.text.HitInfo;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextFlow;
+import javafx.stage.Stage;
+import javafx.stage.StageStyle;
+import javafx.stage.Window;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import test.util.Util;
+
+/*
+ * Test for verifying character index of Text nodes embedded in TextFlow in RTL orientation.
+ *
+ * There are 5 tests in this file.
+ * Here, the scene node orientation is set to RTL for all the tests.
+ *
+ * Steps for testTextAndTextFlowHitInfoForRTLArabicText()
+ * 1. Create a TextFlow. Add a Text nodes with only arabic text.
+ * 2. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 3. Character index should change in ascending order as expected.
+ *
+ * Steps for testTextAndTextFlowHitInfoForRTLEnglishText()
+ * 1. Create a TextFlow. Add a Text nodes with only english text.
+ * 2. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 3. Character index should change in descending order as expected.
+ *
+ * Steps for testTextAndTextFlowHitInfoForRTLMultipleTextNodes()
+ * 1. Create a TextFlow. Add two Text nodes with english and arabic text.
+ * 2. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 3. Character index should change in decreasing order for english text
+ * and in increasing order for arabic text.
+ *
+ * Steps for testTextAndTextFlowHitInfoForRTLMultipleMultiLineEnglishArabicTextNodes()
+ * 1. Create a TextFlow. Add three Text nodes with english and arabic text.
+ * 2. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 3. Character index should change in decreasing order for english text
+ * and in increasing order for arabic text.
+ *
+ * Steps for testTextAndTextFlowHitInfoForRTLMultipleMultiLineEnglishTextNodes()
+ * 1. Create a TextFlow. Add three Text nodes with only english text.
+ * 2. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 3. Character index should change in decreasing order for english text.
+ *
+ * Steps for testTextAndTextFlowHitInfoForRTLMultipleMultiLineArabicTextNodes()
+ * 1. Create a TextFlow. Add three Text nodes with only arabic text.
+ * 2. Move the cursor from right to left with a random
+ * decrement value generated in step() method.
+ * 3. Character index should change in increasing order for arabic text.
+ *
+ */
+
+public class RTLTextFlowCharacterIndexTest {
+ static CountDownLatch startupLatch = new CountDownLatch(1);
+ static Random random;
+ static Robot robot;
+ static TextFlow textFlow;
+ static Text textOne;
+ static Text textTwo;
+ static Text textThree;
+ static VBox vBox;
+
+ static volatile Stage stage;
+ static volatile Scene scene;
+
+ static final int WIDTH = 500;
+ static final int HEIGHT = 200;
+
+ static final int Y_OFFSET = 30;
+ static final int X_LEADING_OFFSET = 10;
+
+ boolean isLeading;
+ boolean textFlowIsLeading;
+ int charIndex;
+ int insertionIndex;
+ int textFlowCharIndex;
+ int textFlowInsertionIndex;
+
+ private void mouseClick(double x, double y) {
+ Util.runAndWait(() -> {
+ Window w = scene.getWindow();
+ robot.mouseMove(w.getX() + scene.getX() + x,
+ w.getY() + scene.getY() + y);
+ robot.mouseClick(MouseButton.PRIMARY);
+ });
+ }
+
+ private void moveMouseOverTextFlow(double x, double y) throws Exception {
+ mouseClick(textFlow.getLayoutX() + x,
+ textFlow.getLayoutY() + y);
+ }
+
+ private void addRTLArabicText() {
+ Util.runAndWait(() -> {
+ textOne.setText("شسيبلاتنم");
+ textOne.setFont(new Font(48));
+ textFlow.getChildren().setAll(textOne);
+ vBox.getChildren().setAll(textFlow);
+ });
+ }
+
+ private void addRTLEnglishText() {
+ Util.runAndWait(() -> {
+ textOne.setText("This is text");
+ textOne.setFont(new Font(48));
+ textFlow.getChildren().setAll(textOne);
+ vBox.getChildren().setAll(textFlow);
+ });
+ }
+
+ private void addMultiNodeRTLEnglishArabicText() {
+ Util.runAndWait(() -> {
+ textOne.setText("Arabic:");
+ textOne.setFont(new Font(48));
+ textTwo.setText("شسيبلاتنم");
+ textTwo.setFont(new Font(48));
+ textFlow.getChildren().setAll(textOne, textTwo);
+ vBox.getChildren().setAll(textFlow);
+ });
+ }
+
+ private void addMultiLineMultiNodeRTLEnglishArabicText() {
+ Util.runAndWait(() -> {
+ textOne.setText("Arabic:");
+ textOne.setFont(new Font(48));
+ textTwo.setText("شسيبلاتنضصثقفغ");
+ textTwo.setFont(new Font(48));
+ textThree.setText("حخهعغقثصضشسيبل");
+ textThree.setFont(new Font(48));
+ textFlow.getChildren().setAll(textOne, textTwo, textThree);
+ vBox.getChildren().setAll(textFlow);
+ });
+ }
+
+ private void addMutliLineMultiNodeRTLEnglishText() {
+ Util.runAndWait(() -> {
+ textOne.setText("First line of text");
+ textOne.setFont(new Font(48));
+ textTwo.setText("Second line of text");
+ textTwo.setFont(new Font(48));
+ textThree.setText("Third line of text");
+ textThree.setFont(new Font(48));
+ textFlow.getChildren().setAll(textOne, textTwo, textThree);
+ vBox.getChildren().setAll(textFlow);
+ });
+ }
+
+ private void addMutliLineMultiNodeRTLArabicText() {
+ Util.runAndWait(() -> {
+ textOne.setText("شسيبلا تنضصثقفغ");
+ textOne.setFont(new Font(48));
+ textTwo.setText("حخهعغقث صضشسيبل");
+ textTwo.setFont(new Font(48));
+ textThree.setText("ضصثقف");
+ textThree.setFont(new Font(48));
+ textFlow.getChildren().setAll(textOne, textTwo, textThree);
+ vBox.getChildren().setAll(textFlow);
+ });
+ }
+
+ @Test
+ public void testTextAndTextFlowHitInfoForRTLArabicText() throws Exception {
+ addRTLArabicText();
+ Util.waitForIdle(scene);
+
+ int textOneLength = textOne.getText().length();
+
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverTextFlow(x, Y_OFFSET);
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ if (textFlowIsLeading) {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex);
+ } else {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < textOneLength);
+ Assertions.assertTrue(textFlowCharIndex < textOneLength);
+ x -= step();
+ }
+ }
+
+ @Test
+ public void testTextAndTextFlowHitInfoForRTLEnglishText() throws Exception {
+ addRTLEnglishText();
+ Util.waitForIdle(scene);
+
+ int textOneLength = textOne.getText().length();
+
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverTextFlow(x, Y_OFFSET);
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ if (textFlowIsLeading) {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex);
+ } else {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < textOneLength);
+ Assertions.assertTrue(textFlowCharIndex < textOneLength);
+ x -= step();
+ }
+ }
+
+ @Test
+ public void testTextAndTextFlowHitInfoForRTLMultipleTextNodes() throws Exception {
+ addMultiNodeRTLEnglishArabicText();
+ Util.waitForIdle(scene);
+
+ int textOneLength = textOne.getText().length();
+ int textTwoLength = textTwo.getText().length();
+
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverTextFlow(x, Y_OFFSET);
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ if (textFlowIsLeading) {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex);
+ } else {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < Math.max(textOneLength, textTwoLength));
+ Assertions.assertTrue(textFlowCharIndex < textOneLength + textTwoLength);
+ x -= step();
+ }
+ }
+
+ @Test
+ public void testTextAndTextFlowHitInfoForRTLMultipleMultiLineEnglishArabicTextNodes() throws Exception {
+ addMultiLineMultiNodeRTLEnglishArabicText();
+ Util.waitForIdle(scene);
+
+ int textOneLength = textOne.getText().length();
+ int textTwoLength = textTwo.getText().length();
+ int textThreeLength = textThree.getText().length();
+
+ for (int y = 0; y < 3; y++) {
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverTextFlow(x, (Y_OFFSET + (Y_OFFSET * (y * 2))));
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ if (textFlowIsLeading) {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex);
+ } else {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < Math.max(textThreeLength, Math.max(textOneLength, textTwoLength)));
+ Assertions.assertTrue(textFlowCharIndex < textOneLength + textTwoLength + textThreeLength);
+ x -= step();
+ }
+ }
+ }
+
+ @Test
+ public void testTextAndTextFlowHitInfoForRTLMultipleMultiLineEnglishTextNodes() throws Exception {
+ addMutliLineMultiNodeRTLEnglishText();
+ Util.waitForIdle(scene);
+
+ int textOneLength = textOne.getText().length();
+ int textTwoLength = textTwo.getText().length();
+ int textThreeLength = textThree.getText().length();
+
+ for (int y = 0; y < 3; y++) {
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverTextFlow(x, (Y_OFFSET + (Y_OFFSET * (y * 2))));
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ if (textFlowIsLeading) {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex);
+ } else {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < Math.max(textThreeLength, Math.max(textOneLength, textTwoLength)));
+ Assertions.assertTrue(textFlowCharIndex < textOneLength + textTwoLength + textThreeLength);
+ x -= step();
+ }
+ }
+ }
+
+ @Test
+ public void testTextAndTextFlowHitInfoForRTLMultipleMultiLineArabicTextNodes() throws Exception {
+ addMutliLineMultiNodeRTLArabicText();
+ Util.waitForIdle(scene);
+
+ int textOneLength = textOne.getText().length();
+ int textTwoLength = textTwo.getText().length();
+ int textThreeLength = textThree.getText().length();
+
+ for (int y = 0; y < 3; y++) {
+ double x = WIDTH - X_LEADING_OFFSET;
+ while (x > X_LEADING_OFFSET) {
+ moveMouseOverTextFlow(x, (Y_OFFSET + (Y_OFFSET * (y * 2))));
+ if (isLeading) {
+ Assertions.assertEquals(charIndex, insertionIndex);
+ } else {
+ Assertions.assertEquals(charIndex, insertionIndex - 1);
+ }
+ if (textFlowIsLeading) {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex);
+ } else {
+ Assertions.assertEquals(textFlowCharIndex, textFlowInsertionIndex - 1);
+ }
+ Assertions.assertTrue(charIndex < Math.max(textThreeLength, Math.max(textOneLength, textTwoLength)));
+ Assertions.assertTrue(textFlowCharIndex < textOneLength + textTwoLength + textThreeLength);
+ x -= step();
+ }
+ }
+ }
+
+ private void handleMouseEvent(MouseEvent event) {
+ PickResult pick = event.getPickResult();
+ Node n = pick.getIntersectedNode();
+
+ if (n != null && n instanceof Text t) {
+ Point3D p3 = pick.getIntersectedPoint();
+ Point2D p = new Point2D(p3.getX(), p3.getY());
+ HitInfo hitInfo = t.hitTest(p);
+
+ isLeading = hitInfo.isLeading();
+ charIndex = hitInfo.getCharIndex();
+ insertionIndex = hitInfo.getInsertionIndex();
+ }
+
+ Point2D point = new Point2D(event.getX(), event.getY());
+ HitInfo textFlowHitInfo = textFlow.hitTest(point);
+ textFlowIsLeading = textFlowHitInfo.isLeading();
+ textFlowCharIndex = textFlowHitInfo.getCharIndex();
+ textFlowInsertionIndex = textFlowHitInfo.getInsertionIndex();
+ }
+
+ private double step() {
+ return 1.0 + random.nextDouble() * 8.0;
+ }
+
+ @AfterEach
+ public void resetUI() {
+ Platform.runLater(() -> {
+ textFlow.removeEventHandler(MouseEvent.MOUSE_PRESSED, this::handleMouseEvent);
+ textOne.removeEventHandler(MouseEvent.MOUSE_PRESSED, this::handleMouseEvent);
+ textTwo.removeEventHandler(MouseEvent.MOUSE_PRESSED, this::handleMouseEvent);
+ textThree.removeEventHandler(MouseEvent.MOUSE_PRESSED, this::handleMouseEvent);
+ });
+ }
+
+ @BeforeEach
+ public void setupUI() {
+ Platform.runLater(() -> {
+ textFlow.addEventHandler(MouseEvent.MOUSE_PRESSED, this::handleMouseEvent);
+ textOne.addEventHandler(MouseEvent.MOUSE_PRESSED, this::handleMouseEvent);
+ textTwo.addEventHandler(MouseEvent.MOUSE_PRESSED, this::handleMouseEvent);
+ textThree.addEventHandler(MouseEvent.MOUSE_PRESSED, this::handleMouseEvent);
+ });
+ }
+
+ @BeforeAll
+ public static void initFX() {
+ long seed = new Random().nextLong();
+ System.out.println("seed=" + seed);
+ random = new Random(seed);
+
+ Util.launch(startupLatch, TestApp.class);
+ }
+
+ @AfterAll
+ public static void exit() {
+ Util.shutdown(stage);
+ }
+
+ public static class TestApp extends Application {
+ @Override
+ public void start(Stage primaryStage) {
+ robot = new Robot();
+ stage = primaryStage;
+
+ textOne = new Text();
+ textTwo = new Text();
+ textThree = new Text();
+ textFlow = new TextFlow();
+ vBox = new VBox();
+
+ scene = new Scene(vBox, WIDTH, HEIGHT);
+ scene.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);
+ stage.setScene(scene);
+ stage.initStyle(StageStyle.UNDECORATED);
+ stage.setOnShown(event -> Platform.runLater(startupLatch::countDown));
+ stage.setAlwaysOnTop(true);
+ stage.show();
+ }
+ }
+}