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(); + } + } +}