From 338a6038cbcc84856edd876e54148934718d460e Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Fri, 20 Dec 2024 14:20:04 -0800 Subject: [PATCH 01/15] initial attempt --- .../com/sun/javafx/scene/text/GlyphList.java | 23 +- .../com/sun/javafx/scene/text/TextSpan.java | 2 +- .../com/sun/javafx/text/PrismTextLayout.java | 5 +- .../java/com/sun/javafx/text/TextRun.java | 28 +- .../com/sun/javafx/pgstub/StubTextLayout.java | 639 ++++++++++++++---- 5 files changed, 565 insertions(+), 132 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/GlyphList.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/GlyphList.java index 7488de48475..638e0d9d969 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/GlyphList.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/GlyphList.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 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 @@ -25,6 +25,7 @@ package com.sun.javafx.scene.text; +import java.util.concurrent.atomic.AtomicBoolean; import com.sun.javafx.geom.Point2D; import com.sun.javafx.geom.RectBounds; @@ -93,5 +94,25 @@ public interface GlyphList { * can be null (for non-rich text) but never null for rich text. */ public TextSpan getTextSpan(); + + /** + * Returns true if this GlyphList is a line break. + * @return whether this GlyphList is a line break + */ + public boolean isLinebreak(); + + /** + * Returns the start offset. + * @return the start offset + */ + public int getStart(); + + /** + * Gets the glyph offset at the specified x coordinate. + * @param x the x coordinate + * @param trailing the reference to hold the trailing flag + * @return the glyph offset + */ + public int getOffsetAtX(float x, AtomicBoolean trailing); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextSpan.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextSpan.java index 4a866ddd9f3..b648dba4b1f 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextSpan.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextSpan.java @@ -46,7 +46,7 @@ public interface TextSpan { public Object getFont(); /** - * The bounds for embedded object, only used the font returns null. + * The bounds for embedded object, only used when the font returns null. * The text for a embedded object should be a single char ("\uFFFC" is * recommended). */ 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 2e482c739ed..0e476cb3bdb 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 @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Hashtable; +import java.util.concurrent.atomic.AtomicBoolean; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; import javafx.scene.shape.PathElement; @@ -448,9 +449,9 @@ public Hit getHitInfo(float x, float y) { } } if (run != null) { - int[] trailing = new int[1]; + AtomicBoolean trailing = new AtomicBoolean(); charIndex = run.getStart() + run.getOffsetAtX(x, trailing); - leading = (trailing[0] == 0); + leading = !trailing.get(); insertionIndex = charIndex; if (getText() != null && insertionIndex < getText().length) { diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/TextRun.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/TextRun.java index 41daf38bfbd..ed615cf9577 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/TextRun.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/TextRun.java @@ -25,6 +25,7 @@ package com.sun.javafx.text; +import java.util.concurrent.atomic.AtomicBoolean; import com.sun.javafx.font.CharToGlyphMapper; import com.sun.javafx.geom.Point2D; import com.sun.javafx.geom.RectBounds; @@ -78,6 +79,7 @@ public TextRun(int start, int length, byte level, boolean complex, if (canonical) flags |= FLAGS_CANONICAL; } + @Override public int getStart() { return start; } @@ -114,6 +116,7 @@ public int getSlot() { return slot; } + @Override public boolean isLinebreak() { return (flags & FLAGS_LINEBREAK) != 0; } @@ -401,39 +404,36 @@ public float getXAtOffset(int offset, boolean leading) { return 0; //line break } - public int getGlyphAtX(float x, int[] trailing) { + public int getGlyphAtX(float x, AtomicBoolean trailing) { boolean ltr = isLeftToRight(); float runX = 0; for (int i = 0; i < glyphCount; i++) { float advance = getAdvance(i); if (runX + advance >= x) { - if (trailing != null) { - //TODO handle clusters - if (x - runX > advance / 2) { - trailing[0] = ltr ? 1 : 0; - } else { - trailing[0] = ltr ? 0 : 1; - } + //TODO handle clusters + if (x - runX > advance / 2) { + trailing.set(ltr ? true : false); + } else { + trailing.set(ltr ? false : true); } return i; } runX += advance; } - if (trailing != null) trailing[0] = ltr ? 1 : 0; + trailing.set(ltr ? true : false); return Math.max(0, glyphCount - 1); } - public int getOffsetAtX(float x, int[] trailing) { + @Override + public int getOffsetAtX(float x, AtomicBoolean trailing) { if (glyphCount > 0) { int glyphIndex = getGlyphAtX(x, trailing); return getCharOffset(glyphIndex); } /* tab */ if (width != -1 && length > 0) { - if (trailing != null) { - if (x > width / 2) { - trailing[0] = 1; - } + if (x > width / 2) { + trailing.set(true); } } return 0; 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 0b21a15015c..dc7680e4e53 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 @@ -25,42 +25,58 @@ package test.com.sun.javafx.pgstub; +import java.text.BreakIterator; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import javafx.scene.shape.PathElement; +import javafx.scene.text.Font; +import com.sun.javafx.font.CharToGlyphMapper; import com.sun.javafx.geom.BaseBounds; import com.sun.javafx.geom.Path2D; +import com.sun.javafx.geom.Point2D; import com.sun.javafx.geom.RectBounds; import com.sun.javafx.geom.Shape; -import com.sun.javafx.scene.text.*; -import javafx.scene.shape.PathElement; -import javafx.scene.text.Font; +import com.sun.javafx.scene.text.GlyphList; +import com.sun.javafx.scene.text.TextLayout; +import com.sun.javafx.scene.text.TextLine; +import com.sun.javafx.scene.text.TextSpan; /** * Stub implementation of the {@link TextLayout} for testing purposes. - *
- * Can calculate the bounds of text by simply using the size of the font. - * If the text is bold, the font will be 1 pixel wider for the calculation. + *

+ * Simulates the text layout by assuming each character is a rectangle + * with the size of the font (bold is 1 pixel wider). + * Expects the text to contain '\n' as line separators. + *

+ * This implementation ignores: alignment, bounds type, and direction. */ public class StubTextLayout implements TextLayout { + private static final double DEFAULT_FONT_SIZE = 10; + private TextSpan[] spans; + private String text; + private Font font; + private int tabSize = DEFAULT_TAB_SIZE; + private float lineSpacing; + private float wrapWidth; + private StubTextLine[] lines; + + public StubTextLayout() { + } @Override public boolean setContent(TextSpan[] spans) { this.spans = spans; - this.text = null; /* Initialized in getText() */ - this.nullFontSize = 10; // need a non-zero size + this.text = null; + lines = null; return true; } - private TextSpan[] spans; - private String text; - private Font font; - private int tabSize = DEFAULT_TAB_SIZE; - private int nullFontSize = 0; - private float spacing; - @Override public boolean setContent(String text, Object font) { this.text = text; final StubFontLoader.StubFont stub = ((StubFontLoader.StubFont)font); this.font = stub == null ? null : stub.font; + lines = null; return true; } @@ -70,19 +86,34 @@ public boolean setAlignment(int alignment) { } @Override - public boolean setWrapWidth(float wrapWidth) { + public boolean setDirection(int direction) { return true; } @Override public boolean setLineSpacing(float spacing) { - this.spacing = spacing; - + this.lineSpacing = spacing; + lines = null; return true; } @Override - public boolean setDirection(int direction) { + public boolean setTabSize(int spaces) { + if (spaces < 1) { + spaces = 1; + } + if (tabSize != spaces) { + tabSize = spaces; + return true; + } + lines = null; + return false; + } + + @Override + public boolean setWrapWidth(float wrapWidth) { + this.wrapWidth = wrapWidth; + lines = null; return true; } @@ -96,151 +127,531 @@ public BaseBounds getBounds() { return getBounds(null, new RectBounds()); } +// @Override +// public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { +// ensureLayout(); +// double fontSizeH = nullFontSize; +// double fontSizeW = nullFontSize; +// if (font != null) { +// fontSizeH = font.getSize(); +// fontSizeW = font.getSize(); +// +// // For better testing, we make bold text a little bit bigger. +// boolean bold = font.getStyle().toLowerCase().contains("bold"); +// if (bold) { +// fontSizeW++; +// } +// } +// +// final String[] lines = getText().split("\n"); +// double width = 0.0; +// double height = fontSizeH * lines.length + lineSpacing * (lines.length - 1); +// for (String line : lines) { +// final int length; +// if (line.contains("\t")) { +// // count chars but when encountering a tab round up to a tabSize boundary +// char [] chrs = line.toCharArray(); +// int spaces = 0; +// for (int i = 0; i < chrs.length; i++) { +// if (chrs[i] == '\t') { +// if (tabSize != 0) { +// while ((++spaces % tabSize) != 0) {} +// } +// } else { +// spaces++; +// } +// } +// length = spaces; +// } else { +// length = line.length(); +// } +// width = Math.max(width, fontSizeW * length); +// } +// return bounds.deriveWithNewBounds(0, (float)-fontSizeH, 0, (float)width, (float)(height-fontSizeH), 0); +// } @Override public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { - double fontSizeH = nullFontSize; - double fontSizeW = nullFontSize; - if (font != null) { - fontSizeH = font.getSize(); - fontSizeW = font.getSize(); - - // For better testing, we make bold text a little bit bigger. - boolean bold = font.getStyle().toLowerCase().contains("bold"); - if (bold) { - fontSizeW++; - } - } - - final String[] lines = getText().split("\n"); - double width = 0.0; - double height = fontSizeH * lines.length + spacing * (lines.length - 1); - for (String line : lines) { - final int length; - if (line.contains("\t")) { - // count chars but when encountering a tab round up to a tabSize boundary - char [] chrs = line.toCharArray(); - int spaces = 0; - for (int i = 0; i < chrs.length; i++) { - if (chrs[i] == '\t') { - if (tabSize != 0) { - while ((++spaces % tabSize) != 0) {} - } - } else { - spaces++; - } + ensureLayout(); + float left = Float.POSITIVE_INFINITY; + float top = Float.POSITIVE_INFINITY; + float right = Float.NEGATIVE_INFINITY; + float bottom = Float.NEGATIVE_INFINITY; + if (filter != null) { + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + GlyphList[] lineRuns = line.getRuns(); + for (int j = 0; j < lineRuns.length; j++) { + GlyphList run = lineRuns[j]; + TextSpan span = run.getTextSpan(); + if (span != filter) continue; + Point2D location = run.getLocation(); + float runLeft = location.x; + //if (run.isLeftBearing()) { + // runLeft += line.getLeftSideBearing(); + //} + float runRight = location.x + run.getWidth(); + //if (run.isRightBearing()) { + // runRight += line.getRightSideBearing(); + //} + float runTop = location.y; + float runBottom = location.y + line.getBounds().getHeight() + lineSpacing; + if (runLeft < left) left = runLeft; + if (runTop < top) top = runTop; + if (runRight > right) right = runRight; + if (runBottom > bottom) bottom = runBottom; } - length = spaces; - } else { - length = line.length(); } - width = Math.max(width, fontSizeW * length); - } - return bounds.deriveWithNewBounds(0, (float)-fontSizeH, 0, - (float)width, (float)(height-fontSizeH), 0); - } - - class StubTextLine implements TextLine { - @Override public GlyphList[] getRuns() { - return new GlyphList[0]; - } - @Override public RectBounds getBounds() { - return new RectBounds(); - } - @Override public float getLeftSideBearing() { - return 0; - } - @Override public float getRightSideBearing() { - return 0; - } - @Override public int getStart() { - return 0; - } - @Override public int getLength() { - return 0; + } else { + top = bottom = 0; + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + RectBounds lineBounds = line.getBounds(); + float lineLeft = lineBounds.getMinX() + line.getLeftSideBearing(); + if (lineLeft < left) left = lineLeft; + float lineRight = lineBounds.getMaxX() + line.getRightSideBearing(); + if (lineRight > right) right = lineRight; + bottom += lineBounds.getHeight(); + } + //if (isMirrored()) { + // float width = getMirroringWidth(); + // float bearing = left; + // left = width - right; + // right = width - bearing; + //} } + return bounds.deriveWithNewBounds(left, top, 0, right, bottom, 0); } @Override public TextLine[] getLines() { - return new TextLine[] {new StubTextLine()}; + ensureLayout(); + return lines; } @Override public GlyphList[] getRuns() { - return new GlyphList[0]; + ensureLayout(); + ArrayList rv = new ArrayList<>(); + for (StubTextLine line : lines) { + for (GlyphList g : line.getRuns()) { + rv.add(g); + } + } + return rv.toArray(GlyphList[]::new); } @Override public Shape getShape(int type, TextSpan filter) { + ensureLayout(); + // all this is undocumented + boolean text = (type & TYPE_TEXT) != 0; + boolean underline = (type & TYPE_UNDERLINE) != 0; + boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; + boolean baselineType = (type & TYPE_BASELINE) != 0; + // TODO return new Path2D(); } + // copied from PrismLayout + // this implementation requires API additions to GlyphList @Override public Hit getHitInfo(float x, float y) { - // TODO this probably needs to be entirely rewritten... - if (getText() == null) { - return new Hit(0, -1, true); - } + ensureLayout(); + int charIndex = -1; + int insertionIndex = -1; + boolean leading = false; - final double fontSize = (font == null ? nullFontSize : font.getSize()); - final String[] lines = text.split("\n"); - int lineIndex = Math.min(lines.length - 1, (int) (y / fontSize)); + ensureLayout(); + int lineIndex = getLineIndex(y); if (lineIndex >= lines.length) { - throw new IllegalStateException("Asked for hit info out of y range: x=" + x + "y=" + - + y + "text='" + text + "', lineIndex=" + lineIndex + ", numLines=" + lines.length + - ", fontSize=" + fontSize); - } - int offset = 0; - for (int i=0; i text.length()) { - throw new IllegalStateException("Asked for hit info out of x range"); + private int getCharCount() { + if (text != null) { + return text.length(); } + int count = 0; + for (int i = 0; i < lines.length; i++) { + count += lines[i].getLength(); + } + return count; + } - return new Hit(offset + charPos, -1, true); + private int getLineIndex(float y) { + int index = 0; + float bottom = 0; + + int lineCount = lines.length; + while (index < lineCount) { + bottom += lines[index].getBounds().getHeight() + lineSpacing; + //if (index + 1 == lineCount) { + // bottom -= lines[index].getLeading(); + //} + if (bottom > y) { + break; + } + index++; + } + return index; } @Override - public PathElement[] getCaretShape(int offset, boolean isLeading, float x, - float y) { + public PathElement[] getCaretShape(int offset, boolean isLeading, float x, float y) { + ensureLayout(); + // TODO return new PathElement[0]; } @Override public PathElement[] getRange(int start, int end, int type, float x, float y) { + ensureLayout(); + // TODO return new PathElement[0]; } @Override public BaseBounds getVisualBounds(int type) { + ensureLayout(); + // TODO return new RectBounds(); } - @Override - public boolean setTabSize(int spaces) { - if (spaces < 1) { - spaces = 1; + private char[] getText() { + char[] text; + int count = 0; + for (int i = 0; i < spans.length; i++) { + count += spans[i].getText().length(); } - if (tabSize != spaces) { - tabSize = spaces; - return true; + text = new char[count]; + int offset = 0; + for (int i = 0; i < spans.length; i++) { + String string = spans[i].getText(); + int length = string.length(); + string.getChars(0, length, text, offset); + offset += length; + } + return text; + } + + private void ensureLayout() { + if (lines == null) { + lines = layout(); + } + } + + private StubTextLine[] layout() { + LayoutBuilder b = new LayoutBuilder(tabSize, lineSpacing, wrapWidth); + if (text != null) { + b.append(null, text, font); + } else if (spans != null) { + for(TextSpan s: spans) { + b.append(s, s.getText(), (Font)s.getFont()); + } + } + return b.getLines(); + } + + /** Text Line */ + private static class StubTextLine implements TextLine { + private final GlyphList[] runs; + private final RectBounds bounds; + private int start; + private int length; + + public StubTextLine(GlyphList[] runs, RectBounds bounds, int start, int length) { + this.runs = runs; + this.bounds = bounds; + this.start = start; + this.length = length; + } + + @Override + public GlyphList[] getRuns() { + return runs; + } + + @Override + public RectBounds getBounds() { + return bounds; + } + + @Override + public float getLeftSideBearing() { + return 0; + } + + @Override + public float getRightSideBearing() { + return 0; + } + + @Override + public int getStart() { + return start; + } + + @Override + public int getLength() { + return length; } - return false; } - private String getText() { - if (text == null) { - if (spans != null) { - StringBuilder sb = new StringBuilder(); - for (TextSpan span : spans) { - sb.append(span.getText()); + /** Glyph List */ + private static class StubGlyphList implements GlyphList { + private final TextSpan span; + private final int start; + private final int length; + private final double x; + private final double y; + private final double charWidth; + private final double charHeight; + private final boolean linebreak; + + public StubGlyphList( + TextSpan span, + int start, + int length, + double x, + double y, + double charWidth, + double charHeight, + boolean linebreak + ) { + this.span = span; + this.start = start; + this.length = length; + this.x = x; + this.y = y; + this.charWidth = charWidth; + this.charHeight = charHeight; + this.linebreak = linebreak; + } + + @Override + public int getStart() { + return start; + } + + @Override + public int getGlyphCount() { + return length; + } + + // this API is rather unclear + @Override + public int getGlyphCode(int glyphIndex) { + // TODO what should it return? for now, let's return the same thing it expects for tab and line break + return CharToGlyphMapper.INVISIBLE_GLYPH_ID; + } + + @Override + public float getPosX(int glyphIndex) { + return (float)(x + glyphIndex * charWidth); + } + + @Override + public float getPosY(int glyphIndex) { + return (float)y; + } + + @Override + public float getWidth() { + return (float)(length * charWidth); + } + + @Override + public float getHeight() { + return (float)charHeight; + } + + @Override + public RectBounds getLineBounds() { + return new RectBounds(0, 0, getWidth(), getHeight()); + } + + @Override + public Point2D getLocation() { + return new Point2D((float)x, (float)y); + } + + @Override + public int getCharOffset(int glyphIndex) { + return start + glyphIndex; + } + + @Override + public boolean isComplex() { + return false; + } + + @Override + public TextSpan getTextSpan() { + return span; + } + + @Override + public boolean isLinebreak() { + return linebreak; + } + + @Override + public int getOffsetAtX(float x, AtomicBoolean trailing) { + double px = Math.max(0.0, x - this.x); + trailing.set(px % charWidth > 0.5); + return (int)(px / charWidth); + } + } + + /** + * Implements a single layout algorithm. + */ + private static class LayoutBuilder { + private final ArrayList lines = new ArrayList<>(); + private final ArrayList runs = new ArrayList<>(); + private final int tabSize; + private final double wrapWidth; + private final double lineSpacing; + private double charHeight; + private double charWidth; + private double x; + private double y; + private double runStartX; + private int runStart; + private int lineStart; + private int column; + private TextSpan span; + private boolean lineBreak; + + public LayoutBuilder(int tabSize, double lineSpacing, double wrapWidth) { + this.tabSize = tabSize; + this.lineSpacing = lineSpacing; + this.wrapWidth = wrapWidth; + } + + public StubTextLine[] getLines() { + if (!runs.isEmpty()) { + addLine(); + } + return lines.toArray(StubTextLine[]::new); + } + + private void addRun(int ix) { + if (ix > 0) { + StubGlyphList r = new StubGlyphList(span, runStart + ix, ix, runStartX, y, charWidth, charHeight, lineBreak); + runs.add(r); + runStart += ix; + runStartX = x; + } + lineBreak = false; + } + + private void addLine() { + int len = runStart - lineStart; + StubGlyphList[] rs = runs.toArray(StubGlyphList[]::new); + RectBounds bounds = new RectBounds(0, 0, (float)x, (float)(charHeight /*+ lineSpacing*/)); + lines.add(new StubTextLine(rs, bounds, lineStart, len)); + + column = 0; + x = 0.0; + runStartX = 0.0; + y += charHeight; + lineStart += len; + } + + public void append(TextSpan span, String text, Font f) { + this.span = span; + charHeight = (f == null) ? DEFAULT_FONT_SIZE : f.getSize(); + charWidth = charHeight; + if (f != null) { + boolean bold = f.getStyle().toLowerCase().contains("bold"); + if (bold) { + charWidth++; + } + } + + int len = text.length(); + for (int i = 0; i < len; i++) { + if(wrapWidth > 0) { + if(x > wrapWidth) { + addRun(i); + addLine(); + } + } + + char c = text.charAt(i); + switch (c) { + case '\t': + addRun(i); + if(tabSize > 0) { + double dw = (tabSize - (column % tabSize)) * charWidth; + x += dw; + } else { + x += charWidth; + column++; + } + i++; + addRun(i); + break; + case '\n': + lineBreak = true; + addRun(i); + addLine(); + continue; + default: + x += charWidth; + column++; + break; } - text = sb.toString(); } } - return text; } } From 6c3aed43c8c73bb80694ac17bee7cc665d4783f8 Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Fri, 20 Dec 2024 14:27:46 -0800 Subject: [PATCH 02/15] whitespace --- .../com/sun/javafx/pgstub/StubTextLayout.java | 48 ++----------------- 1 file changed, 3 insertions(+), 45 deletions(-) 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 dc7680e4e53..14bd0deaf23 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 @@ -127,48 +127,6 @@ public BaseBounds getBounds() { return getBounds(null, new RectBounds()); } -// @Override -// public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { -// ensureLayout(); -// double fontSizeH = nullFontSize; -// double fontSizeW = nullFontSize; -// if (font != null) { -// fontSizeH = font.getSize(); -// fontSizeW = font.getSize(); -// -// // For better testing, we make bold text a little bit bigger. -// boolean bold = font.getStyle().toLowerCase().contains("bold"); -// if (bold) { -// fontSizeW++; -// } -// } -// -// final String[] lines = getText().split("\n"); -// double width = 0.0; -// double height = fontSizeH * lines.length + lineSpacing * (lines.length - 1); -// for (String line : lines) { -// final int length; -// if (line.contains("\t")) { -// // count chars but when encountering a tab round up to a tabSize boundary -// char [] chrs = line.toCharArray(); -// int spaces = 0; -// for (int i = 0; i < chrs.length; i++) { -// if (chrs[i] == '\t') { -// if (tabSize != 0) { -// while ((++spaces % tabSize) != 0) {} -// } -// } else { -// spaces++; -// } -// } -// length = spaces; -// } else { -// length = line.length(); -// } -// width = Math.max(width, fontSizeW * length); -// } -// return bounds.deriveWithNewBounds(0, (float)-fontSizeH, 0, (float)width, (float)(height-fontSizeH), 0); -// } @Override public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { ensureLayout(); @@ -455,7 +413,7 @@ private static class StubGlyphList implements GlyphList { private final double charWidth; private final double charHeight; private final boolean linebreak; - + public StubGlyphList( TextSpan span, int start, @@ -593,7 +551,7 @@ private void addRun(int ix) { } lineBreak = false; } - + private void addLine() { int len = runStart - lineStart; StubGlyphList[] rs = runs.toArray(StubGlyphList[]::new); @@ -626,7 +584,7 @@ public void append(TextSpan span, String text, Font f) { addLine(); } } - + char c = text.charAt(i); switch (c) { case '\t': From 1bcb6bc21043d4996cfe75fb6f0b934f39c0ad79 Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Mon, 23 Dec 2024 08:18:09 -0800 Subject: [PATCH 03/15] add line --- .../test/java/test/com/sun/javafx/pgstub/StubTextLayout.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 14bd0deaf23..0ab788631b7 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 @@ -536,7 +536,7 @@ public LayoutBuilder(int tabSize, double lineSpacing, double wrapWidth) { } public StubTextLine[] getLines() { - if (!runs.isEmpty()) { + if (lines.isEmpty() || !runs.isEmpty()) { addLine(); } return lines.toArray(StubTextLine[]::new); From af248783a3b28233a448c407229e472da7673d9a Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Mon, 23 Dec 2024 15:45:43 -0800 Subject: [PATCH 04/15] spacing --- .../src/main/java/javafx/scene/text/Text.java | 2 +- .../java/test/com/sun/javafx/pgstub/StubTextLayout.java | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) 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 e8cfc6eb806..6fba0d9b1a1 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 @@ -1042,7 +1042,7 @@ public final HitInfo hitTest(Point2D point) { private int findFirstRunStart() { int start = Integer.MAX_VALUE; for (GlyphList r: getRuns()) { - int runStart = ((TextRun) r).getStart(); + int runStart = r.getStart(); if (runStart < start) { start = runStart; } 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 0ab788631b7..d9a9031d94e 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 @@ -130,6 +130,7 @@ public BaseBounds getBounds() { @Override public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { ensureLayout(); + // copied from PrismTextLayout float left = Float.POSITIVE_INFINITY; float top = Float.POSITIVE_INFINITY; float right = Float.NEGATIVE_INFINITY; @@ -324,6 +325,9 @@ public BaseBounds getVisualBounds(int type) { } private char[] getText() { + if (text != null) { + return text.toCharArray(); + } char[] text; int count = 0; for (int i = 0; i < spans.length; i++) { @@ -555,13 +559,13 @@ private void addRun(int ix) { private void addLine() { int len = runStart - lineStart; StubGlyphList[] rs = runs.toArray(StubGlyphList[]::new); - RectBounds bounds = new RectBounds(0, 0, (float)x, (float)(charHeight /*+ lineSpacing*/)); + RectBounds bounds = new RectBounds(0, 0, (float)x, (float)(charHeight)); lines.add(new StubTextLine(rs, bounds, lineStart, len)); column = 0; x = 0.0; runStartX = 0.0; - y += charHeight; + y += (charHeight + lineSpacing); lineStart += len; } From 5cfcd48d137e4c0211c9fc3c15902877fba9e031 Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Wed, 8 Jan 2025 10:56:33 -0800 Subject: [PATCH 05/15] font --- .../java/test/com/sun/javafx/pgstub/StubTextLayout.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 d9a9031d94e..6adbeea614d 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 @@ -356,7 +356,14 @@ private StubTextLine[] layout() { b.append(null, text, font); } else if (spans != null) { for(TextSpan s: spans) { - b.append(s, s.getText(), (Font)s.getFont()); + Object v = s.getFont(); + Font font; + if(v instanceof StubFontLoader.StubFont sf) { + font = sf.font; + } else { + font = (Font)v; + } + b.append(s, s.getText(), font); } } return b.getLines(); From 8216208d0ef09a636104ffd4467c5eb39a284545 Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Wed, 22 Jan 2025 12:49:57 -0800 Subject: [PATCH 06/15] truncated --- .../com/sun/javafx/pgstub/StubTextLayout.java | 59 +++++++++++++++---- .../java/test/javafx/scene/text/TextTest.java | 2 +- 2 files changed, 50 insertions(+), 11 deletions(-) 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 6adbeea614d..2a8928ee806 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -27,6 +27,7 @@ import java.text.BreakIterator; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import javafx.scene.shape.PathElement; import javafx.scene.text.Font; @@ -51,7 +52,9 @@ * This implementation ignores: alignment, bounds type, and direction. */ public class StubTextLayout implements TextLayout { + private static final boolean DEBUG = false; private static final double DEFAULT_FONT_SIZE = 10; + private static final float BASELINE = 0.8f; private TextSpan[] spans; private String text; private Font font; @@ -130,7 +133,7 @@ public BaseBounds getBounds() { @Override public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { ensureLayout(); - // copied from PrismTextLayout + // copied from PrismTextLayout: float left = Float.POSITIVE_INFINITY; float top = Float.POSITIVE_INFINITY; float right = Float.NEGATIVE_INFINITY; @@ -347,6 +350,9 @@ private char[] getText() { private void ensureLayout() { if (lines == null) { lines = layout(); + if(DEBUG) { + System.out.println(List.of(lines)); + } } } @@ -412,6 +418,21 @@ public int getStart() { public int getLength() { return length; } + + @Override + public String toString() { + return "StubTextLine{" + + "start=" + start + + ", length=" + length + + ", bounds={" + + bounds.getMinX() + + "," + bounds.getMinY() + + " " + bounds.getMaxX() + + "," + bounds.getMaxY() + + "}" + + ", runs=" + List.of(runs) + + "}"; + } } /** Glyph List */ @@ -445,6 +466,16 @@ public StubGlyphList( this.linebreak = linebreak; } + @Override + public String toString() { + return "StubGlyphList{" + + "start=" + start + + ", length=" + length + + ", x=" + x + + ", y=" + y + + "}"; + } + @Override public int getStart() { return start; @@ -553,11 +584,11 @@ public StubTextLine[] getLines() { return lines.toArray(StubTextLine[]::new); } - private void addRun(int ix) { - if (ix > 0) { - StubGlyphList r = new StubGlyphList(span, runStart + ix, ix, runStartX, y, charWidth, charHeight, lineBreak); + private void addRun(int offset) { + if (offset > 0) { + StubGlyphList r = new StubGlyphList(span, runStart, offset, runStartX, y, charWidth, charHeight, lineBreak); runs.add(r); - runStart += ix; + runStart = offset; runStartX = x; } lineBreak = false; @@ -566,6 +597,9 @@ private void addRun(int ix) { private void addLine() { int len = runStart - lineStart; StubGlyphList[] rs = runs.toArray(StubGlyphList[]::new); + runs.clear(); + //float baseline = (float)(charHeight * BASELINE); + //RectBounds bounds = new RectBounds(0, -baseline, (float)x, (float)(charHeight) - baseline); RectBounds bounds = new RectBounds(0, 0, (float)x, (float)(charHeight)); lines.add(new StubTextLine(rs, bounds, lineStart, len)); @@ -588,9 +622,10 @@ public void append(TextSpan span, String text, Font f) { } int len = text.length(); - for (int i = 0; i < len; i++) { - if(wrapWidth > 0) { - if(x > wrapWidth) { + int i = 0; + for ( ; i < len; i++) { + if (wrapWidth > 0) { + if (x >= wrapWidth) {// FIX >= addRun(i); addLine(); } @@ -600,7 +635,7 @@ public void append(TextSpan span, String text, Font f) { switch (c) { case '\t': addRun(i); - if(tabSize > 0) { + if (tabSize > 0) { double dw = (tabSize - (column % tabSize)) * charWidth; x += dw; } else { @@ -621,6 +656,10 @@ public void append(TextSpan span, String text, Font f) { break; } } + + if (i > runStart) { + addRun(i); + } } } } diff --git a/modules/javafx.graphics/src/test/java/test/javafx/scene/text/TextTest.java b/modules/javafx.graphics/src/test/java/test/javafx/scene/text/TextTest.java index 14b3ec6d290..f30144e2afd 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/scene/text/TextTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/scene/text/TextTest.java @@ -84,7 +84,7 @@ public void testStoreFont() { Font f = new Font(44); assertEquals(Font.getDefault(), t.getFont()); t.setFont(f); - assertEquals(44f, t.getBaselineOffset(), 0); + assertTrue(t.getBaselineOffset() > (f.getSize() / 2.0)); } // Commented out as StubFontLoader only knows about Amble and its From 5222d1cfdf78e7a733e21cf00fe8a477889c12c4 Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Wed, 22 Jan 2025 13:46:32 -0800 Subject: [PATCH 07/15] glyph run --- .../com/sun/javafx/pgstub/StubTextLayout.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 2a8928ee806..43f0119ed9e 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 @@ -436,7 +436,7 @@ public String toString() { } /** Glyph List */ - private static class StubGlyphList implements GlyphList { + private static class GlyphRun implements GlyphList { private final TextSpan span; private final int start; private final int length; @@ -446,7 +446,7 @@ private static class StubGlyphList implements GlyphList { private final double charHeight; private final boolean linebreak; - public StubGlyphList( + public GlyphRun( TextSpan span, int start, int length, @@ -556,7 +556,7 @@ public int getOffsetAtX(float x, AtomicBoolean trailing) { */ private static class LayoutBuilder { private final ArrayList lines = new ArrayList<>(); - private final ArrayList runs = new ArrayList<>(); + private final ArrayList runs = new ArrayList<>(); private final int tabSize; private final double wrapWidth; private final double lineSpacing; @@ -586,7 +586,7 @@ public StubTextLine[] getLines() { private void addRun(int offset) { if (offset > 0) { - StubGlyphList r = new StubGlyphList(span, runStart, offset, runStartX, y, charWidth, charHeight, lineBreak); + GlyphRun r = new GlyphRun(span, runStart, offset, runStartX, y, charWidth, charHeight, lineBreak); runs.add(r); runStart = offset; runStartX = x; @@ -596,11 +596,11 @@ private void addRun(int offset) { private void addLine() { int len = runStart - lineStart; - StubGlyphList[] rs = runs.toArray(StubGlyphList[]::new); + GlyphRun[] rs = runs.toArray(GlyphRun[]::new); runs.clear(); - //float baseline = (float)(charHeight * BASELINE); - //RectBounds bounds = new RectBounds(0, -baseline, (float)x, (float)(charHeight) - baseline); - RectBounds bounds = new RectBounds(0, 0, (float)x, (float)(charHeight)); + + float baseline = (float)(charHeight * BASELINE); + RectBounds bounds = new RectBounds(0, -baseline, (float)x, (float)(charHeight) - baseline); lines.add(new StubTextLine(rs, bounds, lineStart, len)); column = 0; From d43d51a4f72bed68c731f43bcfc8ddc12ea445e4 Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Thu, 23 Jan 2025 11:53:44 -0800 Subject: [PATCH 08/15] stub fonts --- .../javafx/font/coretext/CTGlyphLayout.java | 8 +- .../font/directwrite/DWGlyphLayout.java | 13 +- .../sun/javafx/font/freetype/FTFactory.java | 9 +- .../javafx/font/freetype/HBGlyphLayout.java | 7 +- .../font/freetype/PangoGlyphLayout.java | 13 +- .../java/com/sun/javafx/text/GlyphLayout.java | 56 +- .../sun/javafx/text/GlyphLayoutManager.java | 76 + .../com/sun/javafx/text/PrismTextLayout.java | 1636 +--------------- .../sun/javafx/text/PrismTextLayoutBase.java | 1674 +++++++++++++++++ .../java/com/sun/javafx/text/TextRun.java | 6 +- .../com/sun/javafx/pgstub/StubFontLoader.java | 16 +- .../sun/javafx/pgstub/StubFontMetrics.java | 104 + .../sun/javafx/pgstub/StubFontResource.java | 196 ++ .../com/sun/javafx/pgstub/StubFontStrike.java | 114 ++ .../sun/javafx/pgstub/StubGlyphLayout.java | 48 + .../com/sun/javafx/pgstub/StubTextLayout.java | 642 +------ .../javafx/scene/layout/BaselineTest.java | 9 +- 17 files changed, 2286 insertions(+), 2341 deletions(-) create mode 100644 modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayoutManager.java create mode 100644 modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutBase.java create mode 100644 modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontMetrics.java create mode 100644 modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java create mode 100644 modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java create mode 100644 modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyphLayout.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyphLayout.java index d8b8d09354c..9ad95bea5c6 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyphLayout.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/coretext/CTGlyphLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -31,6 +31,7 @@ import com.sun.javafx.font.PGFont; import com.sun.javafx.font.PrismFontFactory; import com.sun.javafx.text.GlyphLayout; +import com.sun.javafx.text.GlyphLayoutManager; import com.sun.javafx.text.TextRun; class CTGlyphLayout extends GlyphLayout { @@ -159,4 +160,9 @@ public void layout(TextRun run, PGFont font, FontStrike strike, char[] text) { } OS.CFRelease(lineRef); } + + @Override + public void dispose() { + GlyphLayoutManager.dispose(this); + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/directwrite/DWGlyphLayout.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/directwrite/DWGlyphLayout.java index 64e3d4bf9d6..21d7b158eb8 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/directwrite/DWGlyphLayout.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/directwrite/DWGlyphLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -26,7 +26,6 @@ package com.sun.javafx.font.directwrite; import java.util.Arrays; - import com.sun.javafx.font.CompositeFontResource; import com.sun.javafx.font.FontResource; import com.sun.javafx.font.FontStrike; @@ -34,7 +33,8 @@ import com.sun.javafx.font.PrismFontFactory; import com.sun.javafx.scene.text.TextSpan; import com.sun.javafx.text.GlyphLayout; -import com.sun.javafx.text.PrismTextLayout; +import com.sun.javafx.text.GlyphLayoutManager; +import com.sun.javafx.text.PrismTextLayoutBase; import com.sun.javafx.text.TextRun; public class DWGlyphLayout extends GlyphLayout { @@ -42,7 +42,7 @@ public class DWGlyphLayout extends GlyphLayout { private static final String LOCALE = "en-us"; @Override - protected TextRun addTextRun(PrismTextLayout layout, char[] chars, int start, + protected TextRun addTextRun(PrismTextLayoutBase layout, char[] chars, int start, int length, PGFont font, TextSpan span, byte level) { IDWriteFactory factory = DWFactory.getDWriteFactory(); @@ -407,4 +407,9 @@ private void renderShape(char[] text, TextRun run, PGFont font, int baseSlot) { } format.Release(); } + + @Override + public void dispose() { + GlyphLayoutManager.dispose(this); + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/FTFactory.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/FTFactory.java index 32063c0f64a..dedee4cf6b3 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/FTFactory.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/FTFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -26,7 +26,6 @@ package com.sun.javafx.font.freetype; import java.util.ArrayList; - import com.sun.javafx.PlatformUtil; import com.sun.javafx.font.FontConfigManager; import com.sun.javafx.font.FontFallbackInfo; @@ -36,6 +35,7 @@ import com.sun.javafx.font.PrismFontFactory; import com.sun.javafx.font.PrismFontFile; import com.sun.javafx.text.GlyphLayout; +import com.sun.javafx.text.GlyphLayoutManager; import com.sun.javafx.text.TextRun; public class FTFactory extends PrismFontFactory { @@ -127,6 +127,11 @@ public StubGlyphLayout() { @Override public void layout(TextRun run, PGFont font, FontStrike strike, char[] text) { } + + @Override + public void dispose() { + GlyphLayoutManager.dispose(this); + } } @Override diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/HBGlyphLayout.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/HBGlyphLayout.java index e348fd10776..666ae8f343f 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/HBGlyphLayout.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/HBGlyphLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -28,6 +28,7 @@ import com.sun.javafx.font.FontStrike; import com.sun.javafx.font.PGFont; import com.sun.javafx.text.GlyphLayout; +import com.sun.javafx.text.GlyphLayoutManager; import com.sun.javafx.text.TextRun; public class HBGlyphLayout extends GlyphLayout { @@ -37,4 +38,8 @@ public void layout(TextRun run, PGFont font, FontStrike strike, char[] text) { System.out.println("Only simple text supported."); } + @Override + public void dispose() { + GlyphLayoutManager.dispose(this); + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/PangoGlyphLayout.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/PangoGlyphLayout.java index 59cccae123a..b2e9d04491c 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/PangoGlyphLayout.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/PangoGlyphLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -25,6 +25,9 @@ package com.sun.javafx.font.freetype; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; import com.sun.javafx.font.CompositeFontResource; import com.sun.javafx.font.CompositeGlyphMapper; import com.sun.javafx.font.FontResource; @@ -32,12 +35,9 @@ import com.sun.javafx.font.PGFont; import com.sun.javafx.font.PrismFontFactory; import com.sun.javafx.text.GlyphLayout; +import com.sun.javafx.text.GlyphLayoutManager; import com.sun.javafx.text.TextRun; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.Map; - class PangoGlyphLayout extends GlyphLayout { private static final long fontmap; @@ -205,7 +205,8 @@ public void layout(TextRun run, PGFont font, FontStrike strike, char[] text) { @Override public void dispose() { - super.dispose(); + GlyphLayoutManager.dispose(this); + for (Long str: runUtf8.values()) { OSPango.g_free(str); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java index 6db05961413..0a816551df8 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -113,7 +113,7 @@ public abstract class GlyphLayout { } } - protected TextRun addTextRun(PrismTextLayout layout, char[] chars, + protected TextRun addTextRun(PrismTextLayoutBase layout, char[] chars, int start, int length, PGFont font, TextSpan span, byte level) { /* subclass can overwrite this method in order to handle complex text */ @@ -122,7 +122,7 @@ protected TextRun addTextRun(PrismTextLayout layout, char[] chars, return run; } - private TextRun addTextRun(PrismTextLayout layout, char[] chars, + private TextRun addTextRun(PrismTextLayoutBase layout, char[] chars, int start, int length, PGFont font, TextSpan span, byte level, boolean complex) { @@ -139,7 +139,7 @@ private TextRun addTextRun(PrismTextLayout layout, char[] chars, return run; } - public int breakRuns(PrismTextLayout layout, char[] chars, int flags) { + public int breakRuns(PrismTextLayoutBase layout, char[] chars, int flags) { int length = chars.length; boolean complex = false; boolean feature = false; @@ -384,52 +384,7 @@ protected int getInitialSlot(FontResource fr) { return 0; } - /* This scheme creates a singleton GlyphLayout which is checked out - * for use. Callers who find its checked out create one that after use - * is discarded. This means that in a MT-rendering environment, - * there's no need to synchronise except for that one instance. - * Fewer threads will then need to synchronise, perhaps helping - * throughput on a MP system. If for some reason the reusable - * GlyphLayout is checked out for a long time (or never returned?) then - * we would end up always creating new ones. That situation should not - * occur and if if did, it would just lead to some extra garbage being - * created. - */ - private static GlyphLayout reusableGL = newInstance(); - private static boolean inUse; - - private static GlyphLayout newInstance() { - PrismFontFactory factory = PrismFontFactory.getFontFactory(); - return factory.createGlyphLayout(); - } - - public static GlyphLayout getInstance() { - /* The following heuristic is that if the reusable instance is - * in use, it probably still will be in a micro-second, so avoid - * synchronising on the class and just allocate a new instance. - * The cost is one extra boolean test for the normal case, and some - * small number of cases where we allocate an extra object when - * in fact the reusable one would be freed very soon. - */ - if (inUse) { - return newInstance(); - } else { - synchronized(GlyphLayout.class) { - if (inUse) { - return newInstance(); - } else { - inUse = true; - return reusableGL; - } - } - } - } - - public void dispose() { - if (this == reusableGL) { - inUse = false; - } - } + public abstract void dispose(); private static boolean isIdeographic(int codePoint) { if (isIdeographicMethod != null) { @@ -441,5 +396,4 @@ private static boolean isIdeographic(int codePoint) { } return false; } - } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayoutManager.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayoutManager.java new file mode 100644 index 00000000000..f09dcfa0dd1 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayoutManager.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025, 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 com.sun.javafx.text; + +import com.sun.javafx.font.PrismFontFactory; + +/* This class creates a singleton GlyphLayout which is checked out + * for use. Callers who find its checked out create one that after use + * is discarded. This means that in a MT-rendering environment, + * there's no need to synchronise except for that one instance. + * Fewer threads will then need to synchronise, perhaps helping + * throughput on a MP system. If for some reason the reusable + * GlyphLayout is checked out for a long time (or never returned?) then + * we would end up always creating new ones. That situation should not + * occur and if if did, it would just lead to some extra garbage being + * created. + */ +public class GlyphLayoutManager { + private static GlyphLayout reusableGL = newInstance(); + private static volatile boolean inUse; + + private static GlyphLayout newInstance() { + PrismFontFactory factory = PrismFontFactory.getFontFactory(); + return factory.createGlyphLayout(); + } + + public static GlyphLayout getInstance() { + /* The following heuristic is that if the reusable instance is + * in use, it probably still will be in a micro-second, so avoid + * synchronising on the class and just allocate a new instance. + * The cost is one extra boolean test for the normal case, and some + * small number of cases where we allocate an extra object when + * in fact the reusable one would be freed very soon. + */ + if (inUse) { + return newInstance(); + } else { + synchronized(GlyphLayout.class) { + if (inUse) { + return newInstance(); + } else { + inUse = true; + return reusableGL; + } + } + } + } + + public static void dispose(GlyphLayout la) { + if (la == reusableGL) { + inUse = false; + } + } +} 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 0e476cb3bdb..62b762865c3 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,1640 +25,10 @@ package com.sun.javafx.text; -import java.text.Bidi; -import java.text.BreakIterator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Hashtable; -import java.util.concurrent.atomic.AtomicBoolean; -import javafx.scene.shape.LineTo; -import javafx.scene.shape.MoveTo; -import javafx.scene.shape.PathElement; -import com.sun.javafx.font.CharToGlyphMapper; -import com.sun.javafx.font.FontResource; -import com.sun.javafx.font.FontStrike; -import com.sun.javafx.font.Metrics; -import com.sun.javafx.font.PGFont; import com.sun.javafx.font.PrismFontFactory; -import com.sun.javafx.geom.BaseBounds; -import com.sun.javafx.geom.Path2D; -import com.sun.javafx.geom.Point2D; -import com.sun.javafx.geom.RectBounds; -import com.sun.javafx.geom.RoundRectangle2D; -import com.sun.javafx.geom.Shape; -import com.sun.javafx.geom.transform.BaseTransform; -import com.sun.javafx.geom.transform.Translate2D; -import com.sun.javafx.scene.text.GlyphList; -import com.sun.javafx.scene.text.TextLayout; -import com.sun.javafx.scene.text.TextSpan; - -public class PrismTextLayout implements TextLayout { - private static final BaseTransform IDENTITY = BaseTransform.IDENTITY_TRANSFORM; - private static final int X_MIN_INDEX = 0; - private static final int Y_MIN_INDEX = 1; - private static final int X_MAX_INDEX = 2; - private static final int Y_MAX_INDEX = 3; - - private static final Hashtable stringCache = new Hashtable<>(); - private static final Object CACHE_SIZE_LOCK = new Object(); - private static int cacheSize = 0; - private static final int MAX_STRING_SIZE = 256; - private static final int MAX_CACHE_SIZE = PrismFontFactory.cacheLayoutSize; - - private char[] text; - private TextSpan[] spans; /* Rich text (null for single font text) */ - private PGFont font; /* Single font text (null for rich text) */ - private FontStrike strike; /* cached strike of font (identity) */ - private Integer cacheKey; - private TextLine[] lines; - private TextRun[] runs; - private int runCount; - private BaseBounds logicalBounds; - private RectBounds visualBounds; - private float layoutWidth, layoutHeight; - private float wrapWidth, spacing; - private LayoutCache layoutCache; - private Shape shape; - private int flags; - private int tabSize = DEFAULT_TAB_SIZE; +public class PrismTextLayout extends PrismTextLayoutBase { public PrismTextLayout() { - logicalBounds = new RectBounds(); - flags = ALIGN_LEFT; - } - - private void reset() { - layoutCache = null; - runs = null; - flags &= ~ANALYSIS_MASK; - relayout(); - } - - private void relayout() { - logicalBounds.makeEmpty(); - visualBounds = null; - layoutWidth = layoutHeight = 0; - flags &= ~(FLAGS_WRAPPED | FLAGS_CACHED_UNDERLINE | FLAGS_CACHED_STRIKETHROUGH); - lines = null; - shape = null; - } - - /*************************************************************************** - * * - * TextLayout API * - * * - **************************************************************************/ - - @Override - public boolean setContent(TextSpan[] spans) { - if (spans == null && this.spans == null) return false; - if (spans != null && this.spans != null) { - if (spans.length == this.spans.length) { - int i = 0; - while (i < spans.length) { - if (spans[i] != this.spans[i]) break; - i++; - } - if (i == spans.length) return false; - } - } - - reset(); - this.spans = spans; - this.font = null; - this.strike = null; - this.text = null; /* Initialized in getText() */ - this.cacheKey = null; - return true; - } - - @Override - public boolean setContent(String text, Object font) { - reset(); - this.spans = null; - this.font = (PGFont)font; - this.strike = ((PGFont)font).getStrike(IDENTITY); - this.text = text.toCharArray(); - if (MAX_CACHE_SIZE > 0) { - int length = text.length(); - if (0 < length && length <= MAX_STRING_SIZE) { - cacheKey = text.hashCode() * strike.hashCode(); - } - } - return true; - } - - @Override - public boolean setDirection(int direction) { - if ((flags & DIRECTION_MASK) == direction) return false; - flags &= ~DIRECTION_MASK; - flags |= (direction & DIRECTION_MASK); - reset(); - return true; - } - - @Override - public boolean setBoundsType(int type) { - if ((flags & BOUNDS_MASK) == type) return false; - flags &= ~BOUNDS_MASK; - flags |= (type & BOUNDS_MASK); - reset(); - return true; - } - - @Override - public boolean setAlignment(int alignment) { - int align = ALIGN_LEFT; - switch (alignment) { - case 0: align = ALIGN_LEFT; break; - case 1: align = ALIGN_CENTER; break; - case 2: align = ALIGN_RIGHT; break; - case 3: align = ALIGN_JUSTIFY; break; - } - if ((flags & ALIGN_MASK) == align) return false; - if (align == ALIGN_JUSTIFY || (flags & ALIGN_JUSTIFY) != 0) { - reset(); - } - flags &= ~ALIGN_MASK; - flags |= align; - relayout(); - return true; - } - - @Override - public boolean setWrapWidth(float newWidth) { - if (Float.isInfinite(newWidth)) newWidth = 0; - if (Float.isNaN(newWidth)) newWidth = 0; - float oldWidth = this.wrapWidth; - this.wrapWidth = Math.max(0, newWidth); - - boolean needsLayout = true; - if (lines != null && oldWidth != 0 && newWidth != 0) { - if ((flags & ALIGN_LEFT) != 0) { - if (newWidth > oldWidth) { - /* If wrapping width is increasing and there is no - * wrapped lines then the text remains valid. - */ - if ((flags & FLAGS_WRAPPED) == 0) { - needsLayout = false; - } - } else { - /* If wrapping width is decreasing but it is still - * greater than the max line width then the text - * remains valid. - */ - if (newWidth >= layoutWidth) { - needsLayout = false; - } - } - } - } - if (needsLayout) relayout(); - return needsLayout; - } - - @Override - public boolean setLineSpacing(float spacing) { - if (this.spacing == spacing) return false; - this.spacing = spacing; - relayout(); - return true; - } - - private void ensureLayout() { - if (lines == null) { - layout(); - } - } - - @Override - public com.sun.javafx.scene.text.TextLine[] getLines() { - ensureLayout(); - return lines; - } - - @Override - public GlyphList[] getRuns() { - ensureLayout(); - GlyphList[] result = new GlyphList[runCount]; - int count = 0; - for (int i = 0; i < lines.length; i++) { - GlyphList[] lineRuns = lines[i].getRuns(); - int length = lineRuns.length; - System.arraycopy(lineRuns, 0, result, count, length); - count += length; - } - return result; - } - - @Override - public BaseBounds getBounds() { - ensureLayout(); - return logicalBounds; - } - - @Override - public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { - ensureLayout(); - float left = Float.POSITIVE_INFINITY; - float top = Float.POSITIVE_INFINITY; - float right = Float.NEGATIVE_INFINITY; - float bottom = Float.NEGATIVE_INFINITY; - if (filter != null) { - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - TextRun[] lineRuns = line.getRuns(); - for (int j = 0; j < lineRuns.length; j++) { - TextRun run = lineRuns[j]; - TextSpan span = run.getTextSpan(); - if (span != filter) continue; - Point2D location = run.getLocation(); - float runLeft = location.x; - if (run.isLeftBearing()) { - runLeft += line.getLeftSideBearing(); - } - float runRight = location.x + run.getWidth(); - if (run.isRightBearing()) { - runRight += line.getRightSideBearing(); - } - float runTop = location.y; - float runBottom = location.y + line.getBounds().getHeight() + spacing; - if (runLeft < left) left = runLeft; - if (runTop < top) top = runTop; - if (runRight > right) right = runRight; - if (runBottom > bottom) bottom = runBottom; - } - } - } else { - top = bottom = 0; - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - RectBounds lineBounds = line.getBounds(); - float lineLeft = lineBounds.getMinX() + line.getLeftSideBearing(); - if (lineLeft < left) left = lineLeft; - float lineRight = lineBounds.getMaxX() + line.getRightSideBearing(); - if (lineRight > right) right = lineRight; - bottom += lineBounds.getHeight(); - } - if (isMirrored()) { - float width = getMirroringWidth(); - float bearing = left; - left = width - right; - right = width - bearing; - } - } - return bounds.deriveWithNewBounds(left, top, 0, right, bottom, 0); - } - - @Override - public PathElement[] getCaretShape(int offset, boolean isLeading, - float x, float y) { - ensureLayout(); - int lineIndex = 0; - int lineCount = getLineCount(); - while (lineIndex < lineCount - 1) { - TextLine line = lines[lineIndex]; - int lineEnd = line.getStart() + line.getLength(); - if (lineEnd > offset) break; - lineIndex++; - } - int splitCaretOffset = -1; - int level = 0; - float lineX = 0, lineY = 0, lineHeight = 0; - TextLine line = lines[lineIndex]; - TextRun[] runs = line.getRuns(); - int runCount = runs.length; - int runIndex = -1; - for (int i = 0; i < runCount; i++) { - TextRun run = runs[i]; - int runStart = run.getStart(); - int runEnd = run.getEnd(); - if (runStart <= offset && offset < runEnd) { - if (!run.isLinebreak()) { - runIndex = i; - } - break; - } - } - if (runIndex != -1) { - TextRun run = runs[runIndex]; - int runStart = run.getStart(); - Point2D location = run.getLocation(); - lineX = location.x + run.getXAtOffset(offset - runStart, isLeading); - lineY = location.y; - lineHeight = line.getBounds().getHeight(); - - if (isLeading) { - if (runIndex > 0 && offset == runStart) { - level = run.getLevel(); - splitCaretOffset = offset - 1; - } - } else { - int runEnd = run.getEnd(); - if (runIndex + 1 < runs.length && offset + 1 == runEnd) { - level = run.getLevel(); - splitCaretOffset = offset + 1; - } - } - } else { - /* end of line (line break or offset>=charCount) */ - int maxOffset = 0; - - /* set run index to zero to handle empty line case (only break line) */ - runIndex = 0; - for (int i = 0; i < runCount; i++) { - TextRun run = runs[i]; - /*use the trailing edge of the last logical run*/ - if (run.getStart() >= maxOffset && !run.isLinebreak()) { - maxOffset = run.getStart(); - runIndex = i; - } - } - TextRun run = runs[runIndex]; - Point2D location = run.getLocation(); - lineX = location.x + (run.isLeftToRight() ? run.getWidth() : 0); - lineY = location.y; - lineHeight = line.getBounds().getHeight(); - } - if (isMirrored()) { - lineX = getMirroringWidth() - lineX; - } - lineX += x; - lineY += y; - if (splitCaretOffset != -1) { - for (int i = 0; i < runs.length; i++) { - TextRun run = runs[i]; - int runStart = run.getStart(); - int runEnd = run.getEnd(); - if (runStart <= splitCaretOffset && splitCaretOffset < runEnd) { - if ((run.getLevel() & 1) != (level & 1)) { - Point2D location = run.getLocation(); - float lineX2 = location.x; - if (isLeading) { - if ((level & 1) != 0) lineX2 += run.getWidth(); - } else { - if ((level & 1) == 0) lineX2 += run.getWidth(); - } - if (isMirrored()) { - lineX2 = getMirroringWidth() - lineX2; - } - lineX2 += x; - PathElement[] result = new PathElement[4]; - result[0] = new MoveTo(lineX, lineY); - result[1] = new LineTo(lineX, lineY + lineHeight / 2); - result[2] = new MoveTo(lineX2, lineY + lineHeight / 2); - result[3] = new LineTo(lineX2, lineY + lineHeight); - return result; - } - } - } - } - PathElement[] result = new PathElement[2]; - result[0] = new MoveTo(lineX, lineY); - result[1] = new LineTo(lineX, lineY + lineHeight); - return result; - } - - @Override - public Hit getHitInfo(float x, float y) { - int charIndex = -1; - int insertionIndex = -1; - boolean leading = false; - - ensureLayout(); - int lineIndex = getLineIndex(y); - if (lineIndex >= getLineCount()) { - charIndex = getCharCount(); - insertionIndex = charIndex + 1; - } else { - TextLine line = lines[lineIndex]; - TextRun[] runs = line.getRuns(); - RectBounds bounds = line.getBounds(); - TextRun run = null; - x -= bounds.getMinX(); - 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(); - } - } - if (run != null) { - AtomicBoolean trailing = new AtomicBoolean(); - charIndex = run.getStart() + run.getOffsetAtX(x, trailing); - leading = !trailing.get(); - - insertionIndex = charIndex; - if (getText() != null && insertionIndex < getText().length) { - if (!leading) { - BreakIterator charIterator = BreakIterator.getCharacterInstance(); - charIterator.setText(new String(getText())); - int next = charIterator.following(insertionIndex); - if (next == BreakIterator.DONE) { - insertionIndex += 1; - } else { - insertionIndex = next; - } - } - } else if (!leading) { - insertionIndex += 1; - } - } else { - //empty line, set to line break leading - charIndex = line.getStart(); - leading = true; - insertionIndex = charIndex; - } - } - return new Hit(charIndex, insertionIndex, leading); - } - - @Override - public PathElement[] getRange(int start, int end, int type, - float x, float y) { - ensureLayout(); - int lineCount = getLineCount(); - ArrayList result = new ArrayList<>(); - float lineY = 0; - - for (int lineIndex = 0; lineIndex < lineCount; lineIndex++) { - TextLine line = lines[lineIndex]; - RectBounds lineBounds = line.getBounds(); - int lineStart = line.getStart(); - if (lineStart >= end) break; - int lineEnd = lineStart + line.getLength(); - if (start > lineEnd) { - lineY += lineBounds.getHeight() + spacing; - continue; - } - - /* The list of runs in the line is visually ordered. - * Thus, finding the run that includes the selection end offset - * does not mean that all selected runs have being visited. - * Instead, this implementation first computes the number of selected - * characters in the current line, then iterates over the runs consuming - * selected characters till all of them are found. - */ - TextRun[] runs = line.getRuns(); - int count = Math.min(lineEnd, end) - Math.max(lineStart, start); - int runIndex = 0; - float left = -1; - float right = -1; - float lineX = lineBounds.getMinX(); - while (count > 0 && runIndex < runs.length) { - TextRun run = runs[runIndex]; - int runStart = run.getStart(); - int runEnd = run.getEnd(); - float runWidth = run.getWidth(); - int clmapStart = Math.max(runStart, Math.min(start, runEnd)); - int clampEnd = Math.max(runStart, Math.min(end, runEnd)); - int runCount = clampEnd - clmapStart; - if (runCount != 0) { - boolean ltr = run.isLeftToRight(); - float runLeft; - if (runStart > start) { - runLeft = ltr ? lineX : lineX + runWidth; - } else { - runLeft = lineX + run.getXAtOffset(start - runStart, true); - } - float runRight; - if (runEnd < end) { - runRight = ltr ? lineX + runWidth : lineX; - } else { - runRight = lineX + run.getXAtOffset(end - runStart, true); - } - if (runLeft > runRight) { - float tmp = runLeft; - runLeft = runRight; - runRight = tmp; - } - count -= runCount; - float top = 0, bottom = 0; - switch (type) { - case TYPE_TEXT: - top = lineY; - bottom = lineY + lineBounds.getHeight(); - break; - case TYPE_UNDERLINE: - case TYPE_STRIKETHROUGH: - FontStrike fontStrike = null; - if (spans != null) { - TextSpan span = run.getTextSpan(); - PGFont font = (PGFont)span.getFont(); - if (font == null) break; - fontStrike = font.getStrike(IDENTITY); - } else { - fontStrike = strike; - } - top = lineY - run.getAscent(); - Metrics metrics = fontStrike.getMetrics(); - if (type == TYPE_UNDERLINE) { - top += metrics.getUnderLineOffset(); - bottom = top + metrics.getUnderLineThickness(); - } else { - top += metrics.getStrikethroughOffset(); - bottom = top + metrics.getStrikethroughThickness(); - } - break; - } - - /* Merge continuous rectangles */ - if (runLeft != right) { - if (left != -1 && right != -1) { - float l = left, r = right; - if (isMirrored()) { - float width = getMirroringWidth(); - l = width - l; - r = width - r; - } - result.add(new MoveTo(x + l, y + top)); - result.add(new LineTo(x + r, y + top)); - result.add(new LineTo(x + r, y + bottom)); - result.add(new LineTo(x + l, y + bottom)); - result.add(new LineTo(x + l, y + top)); - } - left = runLeft; - right = runRight; - } - right = runRight; - if (count == 0) { - float l = left, r = right; - if (isMirrored()) { - float width = getMirroringWidth(); - l = width - l; - r = width - r; - } - result.add(new MoveTo(x + l, y + top)); - result.add(new LineTo(x + r, y + top)); - result.add(new LineTo(x + r, y + bottom)); - result.add(new LineTo(x + l, y + bottom)); - result.add(new LineTo(x + l, y + top)); - } - } - lineX += runWidth; - runIndex++; - } - lineY += lineBounds.getHeight() + spacing; - } - return result.toArray(new PathElement[result.size()]); - } - - @Override - public Shape getShape(int type, TextSpan filter) { - ensureLayout(); - boolean text = (type & TYPE_TEXT) != 0; - boolean underline = (type & TYPE_UNDERLINE) != 0; - boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; - boolean baselineType = (type & TYPE_BASELINE) != 0; - if (shape != null && text && !underline && !strikethrough && baselineType) { - return shape; - } - - Path2D outline = new Path2D(); - BaseTransform tx = new Translate2D(0, 0); - /* Return a shape relative to the baseline of the first line so - * it can be used for layout */ - float firstBaseline = 0; - if (baselineType) { - firstBaseline = -lines[0].getBounds().getMinY(); - } - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - TextRun[] runs = line.getRuns(); - RectBounds bounds = line.getBounds(); - float baseline = -bounds.getMinY(); - for (int j = 0; j < runs.length; j++) { - TextRun run = runs[j]; - FontStrike fontStrike = null; - if (spans != null) { - TextSpan span = run.getTextSpan(); - if (filter != null && span != filter) continue; - PGFont font = (PGFont)span.getFont(); - - /* skip embedded runs */ - if (font == null) continue; - fontStrike = font.getStrike(IDENTITY); - } else { - fontStrike = strike; - } - Point2D location = run.getLocation(); - float runX = location.x; - float runY = location.y + baseline - firstBaseline; - Metrics metrics = null; - if (underline || strikethrough) { - metrics = fontStrike.getMetrics(); - } - if (underline) { - RoundRectangle2D rect = new RoundRectangle2D(); - rect.x = runX; - rect.y = runY + metrics.getUnderLineOffset(); - rect.width = run.getWidth(); - rect.height = metrics.getUnderLineThickness(); - outline.append(rect, false); - } - if (strikethrough) { - RoundRectangle2D rect = new RoundRectangle2D(); - rect.x = runX; - rect.y = runY + metrics.getStrikethroughOffset(); - rect.width = run.getWidth(); - rect.height = metrics.getStrikethroughThickness(); - outline.append(rect, false); - } - if (text && run.getGlyphCount() > 0) { - tx.restoreTransform(1, 0, 0, 1, runX, runY); - Path2D path = (Path2D)fontStrike.getOutline(run, tx); - outline.append(path, false); - } - } - } - - if (text && !underline && !strikethrough) { - shape = outline; - } - return outline; - } - - @Override - public boolean setTabSize(int spaces) { - if (spaces < 1) { - spaces = 1; - } - if (tabSize != spaces) { - tabSize = spaces; - relayout(); - return true; - } - return false; - } - - /*************************************************************************** - * * - * Text Layout Implementation * - * * - **************************************************************************/ - - private int getLineIndex(float y) { - int index = 0; - float bottom = 0; - - int lineCount = getLineCount(); - while (index < lineCount) { - bottom += lines[index].getBounds().getHeight() + spacing; - if (index + 1 == lineCount) { - bottom -= lines[index].getLeading(); - } - if (bottom > y) { - break; - } - index++; - } - return index; - } - - private boolean copyCache() { - int align = flags & ALIGN_MASK; - int boundsType = flags & BOUNDS_MASK; - /* Caching for boundsType == Center, bias towards Modena */ - return wrapWidth != 0 || align != ALIGN_LEFT || boundsType == 0 || isMirrored(); - } - - private void initCache() { - if (cacheKey != null) { - if (layoutCache == null) { - LayoutCache cache = stringCache.get(cacheKey); - if (cache != null && cache.font.equals(font) && Arrays.equals(cache.text, text)) { - layoutCache = cache; - runs = cache.runs; - runCount = cache.runCount; - flags |= cache.analysis; - } - } - if (layoutCache != null) { - if (copyCache()) { - /* This instance has some property that requires it to - * build its own lines (i.e. wrapping width). Thus, only use - * the runs from the cache (and it needs to make a copy - * before using it as they will be modified). - * Note: the copy of the elements in the array happens in - * reuseRuns(). - */ - if (layoutCache.runs == runs) { - runs = new TextRun[runCount]; - System.arraycopy(layoutCache.runs, 0, runs, 0, runCount); - } - } else { - if (layoutCache.lines != null) { - runs = layoutCache.runs; - runCount = layoutCache.runCount; - flags |= layoutCache.analysis; - lines = layoutCache.lines; - layoutWidth = layoutCache.layoutWidth; - layoutHeight = layoutCache.layoutHeight; - float ascent = lines[0].getBounds().getMinY(); - logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, - layoutWidth, layoutHeight + ascent, 0); - } - } - } - } - } - - private int getLineCount() { - return lines.length; - } - - private int getCharCount() { - if (text != null) return text.length; - int count = 0; - for (int i = 0; i < lines.length; i++) { - count += lines[i].getLength(); - } - return count; - } - - public TextSpan[] getTextSpans() { - return spans; - } - - public PGFont getFont() { - return font; - } - - public int getDirection() { - if ((flags & DIRECTION_LTR) != 0) { - return Bidi.DIRECTION_LEFT_TO_RIGHT; - } - if ((flags & DIRECTION_RTL) != 0) { - return Bidi.DIRECTION_RIGHT_TO_LEFT; - } - if ((flags & DIRECTION_DEFAULT_LTR) != 0) { - return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; - } - if ((flags & DIRECTION_DEFAULT_RTL) != 0) { - return Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT; - } - return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; - } - - public void addTextRun(TextRun run) { - if (runCount + 1 > runs.length) { - TextRun[] newRuns = new TextRun[runs.length + 64]; - System.arraycopy(runs, 0, newRuns, 0, runs.length); - runs = newRuns; - } - runs[runCount++] = run; - } - - private void buildRuns(char[] chars) { - runCount = 0; - if (runs == null) { - int count = Math.max(4, Math.min(chars.length / 16, 16)); - runs = new TextRun[count]; - } - GlyphLayout layout = GlyphLayout.getInstance(); - flags = layout.breakRuns(this, chars, flags); - layout.dispose(); - for (int j = runCount; j < runs.length; j++) { - runs[j] = null; - } - } - - private void shape(TextRun run, char[] chars, GlyphLayout layout) { - FontStrike strike; - PGFont font; - if (spans != null) { - if (spans.length == 0) return; - TextSpan span = run.getTextSpan(); - font = (PGFont)span.getFont(); - if (font == null) { - RectBounds bounds = span.getBounds(); - run.setEmbedded(bounds, span.getText().length()); - return; - } - strike = font.getStrike(IDENTITY); - } else { - font = this.font; - strike = this.strike; - } - - /* init metrics for line breaks for empty lines */ - if (run.getAscent() == 0) { - Metrics m = strike.getMetrics(); - - /* The implementation of the center layoutBounds mode is to assure the - * layout has the same number of pixels above and bellow the cap - * height. - */ - if ((flags & BOUNDS_MASK) == BOUNDS_CENTER) { - float ascent = m.getAscent(); - /* Segoe UI has a very large internal leading area, applying the - * center layoutBounds heuristics on it would result in several pixels - * being added to the descent. The final results would be - * overly large and visually unappealing. The fix is to reduce - * the ascent before applying the algorithm. */ - if (font.getFamilyName().equals("Segoe UI")) { - ascent *= 0.80; - } - ascent = (int)(ascent-0.75); - float descent = (int)(m.getDescent()+0.75); - float leading = (int)(m.getLineGap()+0.75); - float capHeight = (int)(m.getCapHeight()+0.75); - float topPadding = -ascent - capHeight; - if (topPadding > descent) { - descent = topPadding; - } else { - ascent += (topPadding - descent); - } - run.setMetrics(ascent, descent, leading); - } else { - run.setMetrics(m.getAscent(), m.getDescent(), m.getLineGap()); - } - } - - if (run.isTab()) return; - if (run.isLinebreak()) return; - if (run.getGlyphCount() > 0) return; - if (run.isComplex()) { - /* Use GlyphLayout to shape complex text */ - layout.layout(run, font, strike, chars); - } else { - FontResource fr = strike.getFontResource(); - int start = run.getStart(); - int length = run.getLength(); - - /* No glyph layout required */ - if (layoutCache == null) { - float fontSize = strike.getSize(); - CharToGlyphMapper mapper = fr.getGlyphMapper(); - - /* The text contains complex and non-complex runs */ - int[] glyphs = new int[length]; - mapper.charsToGlyphs(start, length, chars, glyphs); - float[] positions = new float[(length + 1) << 1]; - float xadvance = 0; - for (int i = 0; i < length; i++) { - float width = fr.getAdvance(glyphs[i], fontSize); - positions[i<<1] = xadvance; - //yadvance always zero - xadvance += width; - } - positions[length<<1] = xadvance; - run.shape(length, glyphs, positions, null); - } else { - - /* The text only contains non-complex runs, all the glyphs and - * advances are stored in the shapeCache */ - if (!layoutCache.valid) { - float fontSize = strike.getSize(); - CharToGlyphMapper mapper = fr.getGlyphMapper(); - mapper.charsToGlyphs(start, length, chars, layoutCache.glyphs, start); - int end = start + length; - float width = 0; - for (int i = start; i < end; i++) { - float adv = fr.getAdvance(layoutCache.glyphs[i], fontSize); - layoutCache.advances[i] = adv; - width += adv; - } - run.setWidth(width); - } - run.shape(length, layoutCache.glyphs, layoutCache.advances); - } - } - } - - private TextLine createLine(int start, int end, int startOffset, float collapsedSpaceWidth) { - int count = end - start + 1; - - assert count > 0 : "number of TextRuns in a TextLine cannot be less than one: " + count; - - TextRun[] lineRuns = new TextRun[count]; - if (start < runCount) { - System.arraycopy(runs, start, lineRuns, 0, count); - } - - /* Recompute line width, height, and length (wrapping) */ - float width = 0, ascent = 0, descent = 0, leading = 0; - int length = 0; - for (int i = 0; i < lineRuns.length; i++) { - TextRun run = lineRuns[i]; - width += run.getWidth(); - ascent = Math.min(ascent, run.getAscent()); - descent = Math.max(descent, run.getDescent()); - leading = Math.max(leading, run.getLeading()); - length += run.getLength(); - } - - width -= collapsedSpaceWidth; - - if (width > layoutWidth) layoutWidth = width; - return new TextLine(startOffset, length, lineRuns, - width, ascent, descent, leading); - } - - /** - * Computes the size of the white space trailing a given run. - * - * @param run the run to compute trailing space width for, cannot be {@code null} - * @return the X size of the white space trailing the run - */ - private float computeTrailingSpaceWidth(TextRun run) { - float trailingSpaceWidth = 0; - char[] chars = getText(); - - /* - * As the loop below exits when encountering a non-white space character, - * testing each trailing glyph in turn for white space is safe, as white - * space is always represented with only a single glyph: - */ - - for (int i = run.getGlyphCount() - 1; i >= 0; i--) { - int textOffset = run.getStart() + run.getCharOffset(i); - - if (!Character.isWhitespace(chars[textOffset])) { - break; - } - - trailingSpaceWidth += run.getAdvance(i); - } - - return trailingSpaceWidth; - } - - private void reorderLine(TextLine line) { - TextRun[] runs = line.getRuns(); - int length = runs.length; - if (length > 0 && runs[length - 1].isLinebreak()) { - length--; - } - if (length < 2) return; - byte[] levels = new byte[length]; - for (int i = 0; i < length; i++) { - levels[i] = runs[i].getLevel(); - } - Bidi.reorderVisually(levels, 0, runs, 0, length); - } - - private char[] getText() { - if (text == null) { - int count = 0; - for (int i = 0; i < spans.length; i++) { - count += spans[i].getText().length(); - } - text = new char[count]; - int offset = 0; - for (int i = 0; i < spans.length; i++) { - String string = spans[i].getText(); - int length = string.length(); - string.getChars(0, length, text, offset); - offset += length; - } - } - return text; - } - - private boolean isSimpleLayout() { - int textAlignment = flags & ALIGN_MASK; - boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; - int mask = FLAGS_HAS_BIDI | FLAGS_HAS_COMPLEX; - return (flags & mask) == 0 && !justify; - } - - private boolean isMirrored() { - boolean mirrored = false; - switch (flags & DIRECTION_MASK) { - case DIRECTION_RTL: mirrored = true; break; - case DIRECTION_LTR: mirrored = false; break; - case DIRECTION_DEFAULT_LTR: - case DIRECTION_DEFAULT_RTL: - mirrored = (flags & FLAGS_RTL_BASE) != 0; - } - return mirrored; - } - - private float getMirroringWidth() { - /* The text node in the scene layer is mirrored based on - * result of computeLayoutBounds. The coordinate translation - * in text layout has to be based on the same width. - */ - return wrapWidth != 0 ? wrapWidth : layoutWidth; - } - - private void reuseRuns() { - /* The runs list is always accessed by the same thread (as TextLayout - * is not thread safe) thus it can be modified at any time, but the - * elements inside of the list are shared among threads and cannot be - * modified. Each reused element has to be cloned.*/ - runCount = 0; - int index = 0; - while (index < runs.length) { - TextRun run = runs[index]; - if (run == null) break; - runs[index] = null; - index++; - runs[runCount++] = run = run.unwrap(); - - if (run.isSplit()) { - run.merge(null); /* unmark split */ - while (index < runs.length) { - TextRun nextRun = runs[index]; - if (nextRun == null) break; - run.merge(nextRun); - runs[index] = null; - index++; - if (nextRun.isSplitLast()) break; - } - } - } - } - - private float getTabAdvance() { - float spaceAdvance = 0; - if (spans != null) { - /* Rich text case - use the first font (for now) */ - for (int i = 0; i < spans.length; i++) { - TextSpan span = spans[i]; - PGFont font = (PGFont)span.getFont(); - if (font != null) { - FontStrike strike = font.getStrike(IDENTITY); - spaceAdvance = strike.getCharAdvance(' '); - break; - } - } - } else { - spaceAdvance = strike.getCharAdvance(' '); - } - return tabSize * spaceAdvance; - } - - /* - * The way JavaFX lays out text: - * - * JavaFX distinguishes between soft wraps and hard wraps. Soft wraps - * occur when a wrap width has been set and the text requires wrapping - * to stay within the set wrap width. Hard wraps are explicitly part of - * the text in the form of line feeds (LF) and carriage returns (CR). - * Hard wrapping considers a singular LF or CR, or the combination of - * CR+LF (or LF+CR) as a single wrap location. Hard wrapping also occurs - * between TextSpans when multiple TextSpans were supplied (for wrapping - * purposes, there is no difference between two TextSpans and a single - * TextSpan where the text was concatenated with a line break in between). - * - * Soft wrapping occurs when a wrap width has been set. This occurs at - * the first character that does not fit. - * - * - If that character is not a white space, the break is set immediately - * after the first white space encountered before that character - * - If there is no white space before the preferred break character, the - * break is done at the first character that does not fit (the wrap - * then occurs in the middle of a (long) word) - * - If the preferred break character is white space, and it is followed by - * more white space, the break is moved to the end of the white space (thus - * a break in white space always occurs at first non white space character - * following a white space sequence) - * - * White space collapsing: - * - * Only white space that is present at soft wrapped locations is collapsed to - * zero. Any other white space is preserved. This includes white space between - * words, leading and trailing white space, and white space around hard wrapped - * locations. - * - * Alignment: - * - * The alignment calculation only looks at the width of all the significant - * characters in each line. Significant characters are any non white space - * characters and any white space that has been preserved (white space that wasn't - * collapsed due to soft wrapping). - * - * Alignment does not take text effects, such as strike through and underline, into - * account. This means that such effects can appear unaligned. Trailing spaces at a - * soft wrap location (that are underlined for example), may show the underline go - * outside the logical bounds of the text. - * - * Example, where indicates a soft wrap location, and is a line feed: - * - * " The quick brown fox jumps over the lazy dog " - * - * Would be rendered as (left aligned): - * - * " The quick" - * "brown fox jumps" - * "over the " - * " lazy dog " - * - * The alignment calculation uses the above bounds indicated by the double - * quotes, and so right aligned text would look like: - * - * " The quick" - * "brown fox jumps" - * "over the " - * " lazy dog " - * - * Note that only the white space at the soft wrap locations is collapsed. - * In all other locations the space was preserved (the space between words - * where no soft wrap occurred, the leading and trailing space, and the - * space around the hard wrapped location). - * - * Text effects have no effect on the alignment, and so with underlining on - * the right aligned text would look like: - * - * "___The___quick_" (one collapsed space becomes visible here) - * "brown_fox_jumps__" (two collapsed spaces become visible here) - * "over_the_" - * "_lazy_dog___" - * - * Note that text alignment has not changed at all, but the bounds are exceeded - * in some locations to allow for the underline. Controls displaying such texts - * will likely clip the underlined parts exceeding the bounds. - * - * Users wishing to mitigate some of these perhaps surprising results can ensure - * they use trimmed texts, and avoid the use of line breaks, or at least ensure - * that line breaks are not preceded or succeeded by white space (activating - * line wrapping is not equivalent to collapsing any consecutive white space - * no matter where it occurs). - */ - - private void layout() { - /* Try the cache */ - initCache(); - - /* Whole layout retrieved from the cache */ - if (lines != null) return; - char[] chars = getText(); - - /* runs and runCount are set in reuseRuns or buildRuns */ - if ((flags & FLAGS_ANALYSIS_VALID) != 0 && isSimpleLayout()) { - reuseRuns(); - } else { - buildRuns(chars); - } - - GlyphLayout layout = null; - if ((flags & (FLAGS_HAS_COMPLEX)) != 0) { - layout = GlyphLayout.getInstance(); - } - - float tabAdvance = 0; - if ((flags & FLAGS_HAS_TABS) != 0) { - tabAdvance = getTabAdvance(); - } - - BreakIterator boundary = null; - if (wrapWidth > 0) { - if ((flags & (FLAGS_HAS_COMPLEX | FLAGS_HAS_CJK)) != 0) { - boundary = BreakIterator.getLineInstance(); - boundary.setText(new CharArrayIterator(chars)); - } - } - int textAlignment = flags & ALIGN_MASK; - - /* Optimize simple case: reuse the glyphs and advances as long as the - * text and font are the same. - * The simple case is no bidi, no complex, no justify, no features. - */ - - if (isSimpleLayout()) { - if (layoutCache == null) { - layoutCache = new LayoutCache(); - layoutCache.glyphs = new int[chars.length]; - layoutCache.advances = new float[chars.length]; - } - } else { - layoutCache = null; - } - - float lineWidth = 0; - int startIndex = 0; - int startOffset = 0; - ArrayList linesList = new ArrayList<>(); - for (int i = 0; i < runCount; i++) { - TextRun run = runs[i]; - shape(run, chars, layout); - if (run.isTab()) { - float tabStop = ((int)(lineWidth / tabAdvance) +1) * tabAdvance; - run.setWidth(tabStop - lineWidth); - } - - float runWidth = run.getWidth(); - if (wrapWidth > 0 && lineWidth + runWidth > wrapWidth && !run.isLinebreak()) { - - /* Find offset of the first character that does not fit on the line */ - int hitOffset = run.getStart() + run.getWrapIndex(wrapWidth - lineWidth); - - /* - * Only keep white spaces (not tabs) in the current run to avoid - * dealing with unshaped runs. - * - * If the run is a tab, the run will be always of length 1 (see - * buildRuns()). As there is no "next" character that can be selected - * as the wrap index in this run, the white space skipping logic - * below won't skip tabs. - */ - - int offset = hitOffset; - int runEnd = run.getEnd(); - - // Don't take white space into account at the preferred wrap index: - while (offset + 1 < runEnd && Character.isWhitespace(chars[offset])) { - offset++; - } - - /* Find the break opportunity */ - int breakOffset = offset; - if (boundary != null) { - /* Use Java BreakIterator when complex script are present */ - breakOffset = boundary.isBoundary(offset) || chars[offset] == '\t' ? offset : boundary.preceding(offset); - } else { - /* Simple break strategy for latin text (Performance) */ - boolean currentChar = Character.isWhitespace(chars[breakOffset]); - while (breakOffset > startOffset) { - boolean previousChar = Character.isWhitespace(chars[breakOffset - 1]); - if (!currentChar && previousChar) break; - currentChar = previousChar; - breakOffset--; - } - } - - /* Never break before the line start offset */ - if (breakOffset < startOffset) breakOffset = startOffset; - - /* Find the run that contains the break offset */ - int breakRunIndex = startIndex; - TextRun breakRun = null; - while (breakRunIndex < runCount) { - breakRun = runs[breakRunIndex]; - if (breakRun.getEnd() > breakOffset) break; - breakRunIndex++; - } - - /* No line breaks between hit offset and line start offset. - * Try character wrapping mode at the hit offset. - */ - if (breakOffset == startOffset) { - breakRun = run; - breakRunIndex = i; - breakOffset = hitOffset; - } - - int breakOffsetInRun = breakOffset - breakRun.getStart(); - /* Wrap the entire run to the next (only if it is not the first - * run of the line). - */ - if (breakOffsetInRun == 0 && breakRunIndex != startIndex) { - i = breakRunIndex - 1; - } else { - i = breakRunIndex; - - /* The break offset is at the first offset of the first run of the line. - * This happens when the wrap width is smaller than the width require - * to show the first character for the line. - */ - if (breakOffsetInRun == 0) { - breakOffsetInRun++; - } - if (breakOffsetInRun < breakRun.getLength()) { - if (runCount >= runs.length) { - TextRun[] newRuns = new TextRun[runs.length + 64]; - System.arraycopy(runs, 0, newRuns, 0, i + 1); - System.arraycopy(runs, i + 1, newRuns, i + 2, runs.length - i - 1); - runs = newRuns; - } else { - System.arraycopy(runs, i + 1, runs, i + 2, runCount - i - 1); - } - runs[i + 1] = breakRun.split(breakOffsetInRun); - if (breakRun.isComplex()) { - shape(breakRun, chars, layout); - } - runCount++; - } - } - - /* No point marking the last run of a line a softbreak */ - if (i + 1 < runCount && !runs[i + 1].isLinebreak()) { - run = runs[i]; - run.setSoftbreak(); - flags |= FLAGS_WRAPPED; - - // Tabs should preserve width - - /* - * Due to contextual forms (arabic) it is possible this line - * is still too big since the splitting of the arabic run - * changes the shape of boundary glyphs. For now the - * implementation has opted to have the appropriate - * initial/final shapes and allow those glyphs to - * potentially overlap the wrapping width, rather than use - * the medial form within the wrappingWidth. A better place - * to solve this would be TextRun#getWrapIndex - but its TBD - * there too. - */ - } - } - - lineWidth += runWidth; - if (run.isBreak()) { - TextLine line = createLine(startIndex, i, startOffset, computeTrailingSpaceWidth(runs[i])); - linesList.add(line); - startIndex = i + 1; - startOffset += line.getLength(); - lineWidth = 0; - } - } - if (layout != null) layout.dispose(); - - linesList.add(createLine(startIndex, runCount - 1, startOffset, 0)); - lines = new TextLine[linesList.size()]; - linesList.toArray(lines); - - float fullWidth = wrapWidth > 0 ? wrapWidth : layoutWidth; // layoutWidth = widest line, wrapWidth is user set - float lineY = 0; - float align; - if (isMirrored()) { - align = 1; /* Left and Justify */ - if (textAlignment == ALIGN_RIGHT) align = 0; - } else { - align = 0; /* Left and Justify */ - if (textAlignment == ALIGN_RIGHT) align = 1; - } - if (textAlignment == ALIGN_CENTER) align = 0.5f; - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - int lineStart = line.getStart(); - RectBounds bounds = line.getBounds(); - - /* Center and right alignment */ - float unusedWidth = fullWidth - bounds.getWidth(); - float lineX = unusedWidth * align; - line.setAlignment(lineX); - - /* Justify */ - boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; - if (justify) { - TextRun[] lineRuns = line.getRuns(); - int lineRunCount = lineRuns.length; - if (lineRunCount > 0 && lineRuns[lineRunCount - 1].isSoftbreak()) { - /* count white spaces but skipping trailings whitespaces */ - int lineEnd = lineStart + line.getLength(); - int wsCount = 0; - boolean hitChar = false; - for (int j = lineEnd - 1; j >= lineStart; j--) { - if (!hitChar && chars[j] != ' ') hitChar = true; - if (hitChar && chars[j] == ' ') wsCount++; - } - if (wsCount != 0) { - float inc = unusedWidth / wsCount; - done: - for (int j = 0; j < lineRunCount; j++) { - TextRun textRun = lineRuns[j]; - int runStart = textRun.getStart(); - int runEnd = textRun.getEnd(); - for (int k = runStart; k < runEnd; k++) { - // TODO kashidas - if (chars[k] == ' ') { - textRun.justify(k - runStart, inc); - if (--wsCount == 0) break done; - } - } - } - lineX = 0; - line.setAlignment(lineX); - line.setWidth(fullWidth); - } - } - } - - if ((flags & FLAGS_HAS_BIDI) != 0) { - reorderLine(line); - } - - computeSideBearings(line); - - /* Set run location */ - float runX = lineX; - TextRun[] lineRuns = line.getRuns(); - for (int j = 0; j < lineRuns.length; j++) { - TextRun run = lineRuns[j]; - run.setLocation(runX, lineY); - run.setLine(line); - runX += run.getWidth(); - } - if (i + 1 < lines.length) { - lineY = Math.max(lineY, lineY + bounds.getHeight() + spacing); - } else { - lineY += (bounds.getHeight() - line.getLeading()); - } - } - float ascent = lines[0].getBounds().getMinY(); - layoutHeight = lineY; - logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, layoutWidth, - layoutHeight + ascent, 0); - - - if (layoutCache != null) { - if (cacheKey != null && !layoutCache.valid && !copyCache()) { - /* After layoutCache is added to the stringCache it can be - * accessed by multiple threads. All the data in it must - * be immutable. See copyCache() for the cases where the entire - * layout is immutable. - */ - layoutCache.font = font; - layoutCache.text = text; - layoutCache.runs = runs; - layoutCache.runCount = runCount; - layoutCache.lines = lines; - layoutCache.layoutWidth = layoutWidth; - layoutCache.layoutHeight = layoutHeight; - layoutCache.analysis = flags & ANALYSIS_MASK; - synchronized (CACHE_SIZE_LOCK) { - int charCount = chars.length; - if (cacheSize + charCount > MAX_CACHE_SIZE) { - stringCache.clear(); - cacheSize = 0; - } - stringCache.put(cacheKey, layoutCache); - cacheSize += charCount; - } - } - layoutCache.valid = true; - } - } - - @Override - public BaseBounds getVisualBounds(int type) { - ensureLayout(); - - /* Not defined for rich text */ - if (strike == null) { - return null; - } - - boolean underline = (type & TYPE_UNDERLINE) != 0; - boolean hasUnderline = (flags & FLAGS_CACHED_UNDERLINE) != 0; - boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; - boolean hasStrikethrough = (flags & FLAGS_CACHED_STRIKETHROUGH) != 0; - if (visualBounds != null && underline == hasUnderline - && strikethrough == hasStrikethrough) { - /* Return last cached value */ - return visualBounds; - } - - flags &= ~(FLAGS_CACHED_STRIKETHROUGH | FLAGS_CACHED_UNDERLINE); - if (underline) flags |= FLAGS_CACHED_UNDERLINE; - if (strikethrough) flags |= FLAGS_CACHED_STRIKETHROUGH; - visualBounds = new RectBounds(); - - float xMin = Float.POSITIVE_INFINITY; - float yMin = Float.POSITIVE_INFINITY; - float xMax = Float.NEGATIVE_INFINITY; - float yMax = Float.NEGATIVE_INFINITY; - float bounds[] = new float[4]; - FontResource fr = strike.getFontResource(); - Metrics metrics = strike.getMetrics(); - float size = strike.getSize(); - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - TextRun[] runs = line.getRuns(); - for (int j = 0; j < runs.length; j++) { - TextRun run = runs[j]; - Point2D pt = run.getLocation(); - if (run.isLinebreak()) continue; - int glyphCount = run.getGlyphCount(); - for (int gi = 0; gi < glyphCount; gi++) { - int gc = run.getGlyphCode(gi); - if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { - fr.getGlyphBoundingBox(run.getGlyphCode(gi), size, bounds); - if (bounds[X_MIN_INDEX] != bounds[X_MAX_INDEX]) { - float glyphX = pt.x + run.getPosX(gi); - float glyphY = pt.y + run.getPosY(gi); - float glyphMinX = glyphX + bounds[X_MIN_INDEX]; - float glyphMinY = glyphY - bounds[Y_MAX_INDEX]; - float glyphMaxX = glyphX + bounds[X_MAX_INDEX]; - float glyphMaxY = glyphY - bounds[Y_MIN_INDEX]; - if (glyphMinX < xMin) xMin = glyphMinX; - if (glyphMinY < yMin) yMin = glyphMinY; - if (glyphMaxX > xMax) xMax = glyphMaxX; - if (glyphMaxY > yMax) yMax = glyphMaxY; - } - } - } - if (underline) { - float underlineMinX = pt.x; - float underlineMinY = pt.y + metrics.getUnderLineOffset(); - float underlineMaxX = underlineMinX + run.getWidth(); - float underlineMaxY = underlineMinY + metrics.getUnderLineThickness(); - if (underlineMinX < xMin) xMin = underlineMinX; - if (underlineMinY < yMin) yMin = underlineMinY; - if (underlineMaxX > xMax) xMax = underlineMaxX; - if (underlineMaxY > yMax) yMax = underlineMaxY; - } - if (strikethrough) { - float strikethroughMinX = pt.x; - float strikethroughMinY = pt.y + metrics.getStrikethroughOffset(); - float strikethroughMaxX = strikethroughMinX + run.getWidth(); - float strikethroughMaxY = strikethroughMinY + metrics.getStrikethroughThickness(); - if (strikethroughMinX < xMin) xMin = strikethroughMinX; - if (strikethroughMinY < yMin) yMin = strikethroughMinY; - if (strikethroughMaxX > xMax) xMax = strikethroughMaxX; - if (strikethroughMaxY > yMax) yMax = strikethroughMaxY; - } - } - } - - if (xMin < xMax && yMin < yMax) { - visualBounds.setBounds(xMin, yMin, xMax, yMax); - } - return visualBounds; - } - - private void computeSideBearings(TextLine line) { - TextRun[] runs = line.getRuns(); - if (runs.length == 0) return; - float bounds[] = new float[4]; - FontResource defaultFontResource = null; - float size = 0; - if (strike != null) { - defaultFontResource = strike.getFontResource(); - size = strike.getSize(); - } - - /* The line lsb is the lsb of the first visual character in the line */ - float lsb = 0; - float width = 0; - lsbdone: - for (int i = 0; i < runs.length; i++) { - TextRun run = runs[i]; - int glyphCount = run.getGlyphCount(); - for (int gi = 0; gi < glyphCount; gi++) { - float advance = run.getAdvance(gi); - /* Skip any leading zero-width glyphs in the line */ - if (advance != 0) { - int gc = run.getGlyphCode(gi); - /* Skip any leading invisible glyphs in the line */ - if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { - FontResource fr = defaultFontResource; - if (fr == null) { - TextSpan span = run.getTextSpan(); - PGFont font = (PGFont)span.getFont(); - /* No need to check font != null (run.glyphCount > 0) */ - size = font.getSize(); - fr = font.getFontResource(); - } - fr.getGlyphBoundingBox(gc, size, bounds); - float glyphLsb = bounds[X_MIN_INDEX]; - lsb = Math.min(0, glyphLsb + width); - run.setLeftBearing(); - break lsbdone; - } - } - width += advance; - } - // tabs - if (glyphCount == 0) { - width += run.getWidth(); - } - } - - /* The line rsb is the rsb of the last visual character in the line */ - float rsb = 0; - width = 0; - rsbdone: - for (int i = runs.length - 1; i >= 0 ; i--) { - TextRun run = runs[i]; - int glyphCount = run.getGlyphCount(); - for (int gi = glyphCount - 1; gi >= 0; gi--) { - float advance = run.getAdvance(gi); - /* Skip any trailing zero-width glyphs in the line */ - if (advance != 0) { - int gc = run.getGlyphCode(gi); - /* Skip any trailing invisible glyphs in the line */ - if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { - FontResource fr = defaultFontResource; - if (fr == null) { - TextSpan span = run.getTextSpan(); - PGFont font = (PGFont)span.getFont(); - /* No need to check font != null (run.glyphCount > 0) */ - size = font.getSize(); - fr = font.getFontResource(); - } - fr.getGlyphBoundingBox(gc, size, bounds); - float glyphRsb = bounds[X_MAX_INDEX] - advance; - rsb = Math.max(0, glyphRsb - width); - run.setRightBearing(); - break rsbdone; - } - } - width += advance; - } - // tabs - if (glyphCount == 0) { - width += run.getWidth(); - } - } - line.setSideBearings(lsb, rsb); + super(PrismFontFactory.cacheLayoutSize, GlyphLayoutManager::getInstance); } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutBase.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutBase.java new file mode 100644 index 00000000000..581e59e8689 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutBase.java @@ -0,0 +1,1674 @@ +/* + * Copyright (c) 2012, 2025, 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 com.sun.javafx.text; + +import java.text.Bidi; +import java.text.BreakIterator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Hashtable; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.PathElement; +import com.sun.javafx.font.CharToGlyphMapper; +import com.sun.javafx.font.FontResource; +import com.sun.javafx.font.FontStrike; +import com.sun.javafx.font.Metrics; +import com.sun.javafx.font.PGFont; +import com.sun.javafx.geom.BaseBounds; +import com.sun.javafx.geom.Path2D; +import com.sun.javafx.geom.Point2D; +import com.sun.javafx.geom.RectBounds; +import com.sun.javafx.geom.RoundRectangle2D; +import com.sun.javafx.geom.Shape; +import com.sun.javafx.geom.transform.BaseTransform; +import com.sun.javafx.geom.transform.Translate2D; +import com.sun.javafx.scene.text.GlyphList; +import com.sun.javafx.scene.text.TextLayout; +import com.sun.javafx.scene.text.TextSpan; + +/** + * Original name: PrismTextLayout + */ +public class PrismTextLayoutBase implements TextLayout { + private static final BaseTransform IDENTITY = BaseTransform.IDENTITY_TRANSFORM; + private static final int X_MIN_INDEX = 0; + private static final int Y_MIN_INDEX = 1; + private static final int X_MAX_INDEX = 2; + private static final int Y_MAX_INDEX = 3; + + private static final Hashtable stringCache = new Hashtable<>(); + private static final Object CACHE_SIZE_LOCK = new Object(); + private static int cacheSize = 0; + private static final int MAX_STRING_SIZE = 256; + private final int MAX_CACHE_SIZE; + private final Supplier layoutSupplier; + + private char[] text; + private TextSpan[] spans; /* Rich text (null for single font text) */ + private PGFont font; /* Single font text (null for rich text) */ + private FontStrike strike; /* cached strike of font (identity) */ + private Integer cacheKey; + private TextLine[] lines; + private TextRun[] runs; + private int runCount; + private BaseBounds logicalBounds; + private RectBounds visualBounds; + private float layoutWidth, layoutHeight; + private float wrapWidth, spacing; + private LayoutCache layoutCache; + private Shape shape; + private int flags; + private int tabSize = DEFAULT_TAB_SIZE; + + public PrismTextLayoutBase(int maxCacheSize, Supplier ls) { + MAX_CACHE_SIZE = maxCacheSize; + layoutSupplier = ls; + logicalBounds = new RectBounds(); + flags = ALIGN_LEFT; + } + + private void reset() { + layoutCache = null; + runs = null; + flags &= ~ANALYSIS_MASK; + relayout(); + } + + private void relayout() { + logicalBounds.makeEmpty(); + visualBounds = null; + layoutWidth = layoutHeight = 0; + flags &= ~(FLAGS_WRAPPED | FLAGS_CACHED_UNDERLINE | FLAGS_CACHED_STRIKETHROUGH); + lines = null; + shape = null; + } + + /*************************************************************************** + * * + * TextLayout API * + * * + **************************************************************************/ + + @Override + public boolean setContent(TextSpan[] spans) { + if (spans == null && this.spans == null) return false; + if (spans != null && this.spans != null) { + if (spans.length == this.spans.length) { + int i = 0; + while (i < spans.length) { + if (spans[i] != this.spans[i]) break; + i++; + } + if (i == spans.length) return false; + } + } + + reset(); + this.spans = spans; + this.font = null; + this.strike = null; + this.text = null; /* Initialized in getText() */ + this.cacheKey = null; + return true; + } + + @Override + public boolean setContent(String text, Object font) { + reset(); + this.spans = null; + this.font = (PGFont)font; + this.strike = ((PGFont)font).getStrike(IDENTITY); + this.text = text.toCharArray(); + if (MAX_CACHE_SIZE > 0) { + int length = text.length(); + if (0 < length && length <= MAX_STRING_SIZE) { + cacheKey = text.hashCode() * strike.hashCode(); + } + } + return true; + } + + @Override + public boolean setDirection(int direction) { + if ((flags & DIRECTION_MASK) == direction) return false; + flags &= ~DIRECTION_MASK; + flags |= (direction & DIRECTION_MASK); + reset(); + return true; + } + + @Override + public boolean setBoundsType(int type) { + if ((flags & BOUNDS_MASK) == type) return false; + flags &= ~BOUNDS_MASK; + flags |= (type & BOUNDS_MASK); + reset(); + return true; + } + + @Override + public boolean setAlignment(int alignment) { + int align = ALIGN_LEFT; + switch (alignment) { + case 0: align = ALIGN_LEFT; break; + case 1: align = ALIGN_CENTER; break; + case 2: align = ALIGN_RIGHT; break; + case 3: align = ALIGN_JUSTIFY; break; + } + if ((flags & ALIGN_MASK) == align) return false; + if (align == ALIGN_JUSTIFY || (flags & ALIGN_JUSTIFY) != 0) { + reset(); + } + flags &= ~ALIGN_MASK; + flags |= align; + relayout(); + return true; + } + + @Override + public boolean setWrapWidth(float newWidth) { + if (Float.isInfinite(newWidth)) newWidth = 0; + if (Float.isNaN(newWidth)) newWidth = 0; + float oldWidth = this.wrapWidth; + this.wrapWidth = Math.max(0, newWidth); + + boolean needsLayout = true; + if (lines != null && oldWidth != 0 && newWidth != 0) { + if ((flags & ALIGN_LEFT) != 0) { + if (newWidth > oldWidth) { + /* If wrapping width is increasing and there is no + * wrapped lines then the text remains valid. + */ + if ((flags & FLAGS_WRAPPED) == 0) { + needsLayout = false; + } + } else { + /* If wrapping width is decreasing but it is still + * greater than the max line width then the text + * remains valid. + */ + if (newWidth >= layoutWidth) { + needsLayout = false; + } + } + } + } + if (needsLayout) relayout(); + return needsLayout; + } + + @Override + public boolean setLineSpacing(float spacing) { + if (this.spacing == spacing) return false; + this.spacing = spacing; + relayout(); + return true; + } + + private void ensureLayout() { + if (lines == null) { + layout(); + } + } + + @Override + public com.sun.javafx.scene.text.TextLine[] getLines() { + ensureLayout(); + return lines; + } + + @Override + public GlyphList[] getRuns() { + ensureLayout(); + GlyphList[] result = new GlyphList[runCount]; + int count = 0; + for (int i = 0; i < lines.length; i++) { + GlyphList[] lineRuns = lines[i].getRuns(); + int length = lineRuns.length; + System.arraycopy(lineRuns, 0, result, count, length); + count += length; + } + return result; + } + + @Override + public BaseBounds getBounds() { + ensureLayout(); + return logicalBounds; + } + + @Override + public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { + ensureLayout(); + float left = Float.POSITIVE_INFINITY; + float top = Float.POSITIVE_INFINITY; + float right = Float.NEGATIVE_INFINITY; + float bottom = Float.NEGATIVE_INFINITY; + if (filter != null) { + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + TextRun[] lineRuns = line.getRuns(); + for (int j = 0; j < lineRuns.length; j++) { + TextRun run = lineRuns[j]; + TextSpan span = run.getTextSpan(); + if (span != filter) continue; + Point2D location = run.getLocation(); + float runLeft = location.x; + if (run.isLeftBearing()) { + runLeft += line.getLeftSideBearing(); + } + float runRight = location.x + run.getWidth(); + if (run.isRightBearing()) { + runRight += line.getRightSideBearing(); + } + float runTop = location.y; + float runBottom = location.y + line.getBounds().getHeight() + spacing; + if (runLeft < left) left = runLeft; + if (runTop < top) top = runTop; + if (runRight > right) right = runRight; + if (runBottom > bottom) bottom = runBottom; + } + } + } else { + top = bottom = 0; + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + RectBounds lineBounds = line.getBounds(); + float lineLeft = lineBounds.getMinX() + line.getLeftSideBearing(); + if (lineLeft < left) left = lineLeft; + float lineRight = lineBounds.getMaxX() + line.getRightSideBearing(); + if (lineRight > right) right = lineRight; + bottom += lineBounds.getHeight(); + } + if (isMirrored()) { + float width = getMirroringWidth(); + float bearing = left; + left = width - right; + right = width - bearing; + } + } + return bounds.deriveWithNewBounds(left, top, 0, right, bottom, 0); + } + + @Override + public PathElement[] getCaretShape(int offset, boolean isLeading, + float x, float y) { + ensureLayout(); + int lineIndex = 0; + int lineCount = getLineCount(); + while (lineIndex < lineCount - 1) { + TextLine line = lines[lineIndex]; + int lineEnd = line.getStart() + line.getLength(); + if (lineEnd > offset) break; + lineIndex++; + } + int splitCaretOffset = -1; + int level = 0; + float lineX = 0, lineY = 0, lineHeight = 0; + TextLine line = lines[lineIndex]; + TextRun[] runs = line.getRuns(); + int runCount = runs.length; + int runIndex = -1; + for (int i = 0; i < runCount; i++) { + TextRun run = runs[i]; + int runStart = run.getStart(); + int runEnd = run.getEnd(); + if (runStart <= offset && offset < runEnd) { + if (!run.isLinebreak()) { + runIndex = i; + } + break; + } + } + if (runIndex != -1) { + TextRun run = runs[runIndex]; + int runStart = run.getStart(); + Point2D location = run.getLocation(); + lineX = location.x + run.getXAtOffset(offset - runStart, isLeading); + lineY = location.y; + lineHeight = line.getBounds().getHeight(); + + if (isLeading) { + if (runIndex > 0 && offset == runStart) { + level = run.getLevel(); + splitCaretOffset = offset - 1; + } + } else { + int runEnd = run.getEnd(); + if (runIndex + 1 < runs.length && offset + 1 == runEnd) { + level = run.getLevel(); + splitCaretOffset = offset + 1; + } + } + } else { + /* end of line (line break or offset>=charCount) */ + int maxOffset = 0; + + /* set run index to zero to handle empty line case (only break line) */ + runIndex = 0; + for (int i = 0; i < runCount; i++) { + TextRun run = runs[i]; + /*use the trailing edge of the last logical run*/ + if (run.getStart() >= maxOffset && !run.isLinebreak()) { + maxOffset = run.getStart(); + runIndex = i; + } + } + TextRun run = runs[runIndex]; + Point2D location = run.getLocation(); + lineX = location.x + (run.isLeftToRight() ? run.getWidth() : 0); + lineY = location.y; + lineHeight = line.getBounds().getHeight(); + } + if (isMirrored()) { + lineX = getMirroringWidth() - lineX; + } + lineX += x; + lineY += y; + if (splitCaretOffset != -1) { + for (int i = 0; i < runs.length; i++) { + TextRun run = runs[i]; + int runStart = run.getStart(); + int runEnd = run.getEnd(); + if (runStart <= splitCaretOffset && splitCaretOffset < runEnd) { + if ((run.getLevel() & 1) != (level & 1)) { + Point2D location = run.getLocation(); + float lineX2 = location.x; + if (isLeading) { + if ((level & 1) != 0) lineX2 += run.getWidth(); + } else { + if ((level & 1) == 0) lineX2 += run.getWidth(); + } + if (isMirrored()) { + lineX2 = getMirroringWidth() - lineX2; + } + lineX2 += x; + PathElement[] result = new PathElement[4]; + result[0] = new MoveTo(lineX, lineY); + result[1] = new LineTo(lineX, lineY + lineHeight / 2); + result[2] = new MoveTo(lineX2, lineY + lineHeight / 2); + result[3] = new LineTo(lineX2, lineY + lineHeight); + return result; + } + } + } + } + PathElement[] result = new PathElement[2]; + result[0] = new MoveTo(lineX, lineY); + result[1] = new LineTo(lineX, lineY + lineHeight); + return result; + } + + @Override + public Hit getHitInfo(float x, float y) { + int charIndex = -1; + int insertionIndex = -1; + boolean leading = false; + + ensureLayout(); + int lineIndex = getLineIndex(y); + if (lineIndex >= getLineCount()) { + charIndex = getCharCount(); + insertionIndex = charIndex + 1; + } else { + TextLine line = lines[lineIndex]; + TextRun[] runs = line.getRuns(); + RectBounds bounds = line.getBounds(); + TextRun run = null; + x -= bounds.getMinX(); + 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(); + } + } + if (run != null) { + AtomicBoolean trailing = new AtomicBoolean(); + charIndex = run.getStart() + run.getOffsetAtX(x, trailing); + leading = !trailing.get(); + + insertionIndex = charIndex; + if (getText() != null && insertionIndex < getText().length) { + if (!leading) { + BreakIterator charIterator = BreakIterator.getCharacterInstance(); + charIterator.setText(new String(getText())); + int next = charIterator.following(insertionIndex); + if (next == BreakIterator.DONE) { + insertionIndex += 1; + } else { + insertionIndex = next; + } + } + } else if (!leading) { + insertionIndex += 1; + } + } else { + //empty line, set to line break leading + charIndex = line.getStart(); + leading = true; + insertionIndex = charIndex; + } + } + return new Hit(charIndex, insertionIndex, leading); + } + + @Override + public PathElement[] getRange(int start, int end, int type, + float x, float y) { + ensureLayout(); + int lineCount = getLineCount(); + ArrayList result = new ArrayList<>(); + float lineY = 0; + + for (int lineIndex = 0; lineIndex < lineCount; lineIndex++) { + TextLine line = lines[lineIndex]; + RectBounds lineBounds = line.getBounds(); + int lineStart = line.getStart(); + if (lineStart >= end) break; + int lineEnd = lineStart + line.getLength(); + if (start > lineEnd) { + lineY += lineBounds.getHeight() + spacing; + continue; + } + + /* The list of runs in the line is visually ordered. + * Thus, finding the run that includes the selection end offset + * does not mean that all selected runs have being visited. + * Instead, this implementation first computes the number of selected + * characters in the current line, then iterates over the runs consuming + * selected characters till all of them are found. + */ + TextRun[] runs = line.getRuns(); + int count = Math.min(lineEnd, end) - Math.max(lineStart, start); + int runIndex = 0; + float left = -1; + float right = -1; + float lineX = lineBounds.getMinX(); + while (count > 0 && runIndex < runs.length) { + TextRun run = runs[runIndex]; + int runStart = run.getStart(); + int runEnd = run.getEnd(); + float runWidth = run.getWidth(); + int clmapStart = Math.max(runStart, Math.min(start, runEnd)); + int clampEnd = Math.max(runStart, Math.min(end, runEnd)); + int runCount = clampEnd - clmapStart; + if (runCount != 0) { + boolean ltr = run.isLeftToRight(); + float runLeft; + if (runStart > start) { + runLeft = ltr ? lineX : lineX + runWidth; + } else { + runLeft = lineX + run.getXAtOffset(start - runStart, true); + } + float runRight; + if (runEnd < end) { + runRight = ltr ? lineX + runWidth : lineX; + } else { + runRight = lineX + run.getXAtOffset(end - runStart, true); + } + if (runLeft > runRight) { + float tmp = runLeft; + runLeft = runRight; + runRight = tmp; + } + count -= runCount; + float top = 0, bottom = 0; + switch (type) { + case TYPE_TEXT: + top = lineY; + bottom = lineY + lineBounds.getHeight(); + break; + case TYPE_UNDERLINE: + case TYPE_STRIKETHROUGH: + FontStrike fontStrike = null; + if (spans != null) { + TextSpan span = run.getTextSpan(); + PGFont font = (PGFont)span.getFont(); + if (font == null) break; + fontStrike = font.getStrike(IDENTITY); + } else { + fontStrike = strike; + } + top = lineY - run.getAscent(); + Metrics metrics = fontStrike.getMetrics(); + if (type == TYPE_UNDERLINE) { + top += metrics.getUnderLineOffset(); + bottom = top + metrics.getUnderLineThickness(); + } else { + top += metrics.getStrikethroughOffset(); + bottom = top + metrics.getStrikethroughThickness(); + } + break; + } + + /* Merge continuous rectangles */ + if (runLeft != right) { + if (left != -1 && right != -1) { + float l = left, r = right; + if (isMirrored()) { + float width = getMirroringWidth(); + l = width - l; + r = width - r; + } + result.add(new MoveTo(x + l, y + top)); + result.add(new LineTo(x + r, y + top)); + result.add(new LineTo(x + r, y + bottom)); + result.add(new LineTo(x + l, y + bottom)); + result.add(new LineTo(x + l, y + top)); + } + left = runLeft; + right = runRight; + } + right = runRight; + if (count == 0) { + float l = left, r = right; + if (isMirrored()) { + float width = getMirroringWidth(); + l = width - l; + r = width - r; + } + result.add(new MoveTo(x + l, y + top)); + result.add(new LineTo(x + r, y + top)); + result.add(new LineTo(x + r, y + bottom)); + result.add(new LineTo(x + l, y + bottom)); + result.add(new LineTo(x + l, y + top)); + } + } + lineX += runWidth; + runIndex++; + } + lineY += lineBounds.getHeight() + spacing; + } + return result.toArray(new PathElement[result.size()]); + } + + @Override + public Shape getShape(int type, TextSpan filter) { + ensureLayout(); + boolean text = (type & TYPE_TEXT) != 0; + boolean underline = (type & TYPE_UNDERLINE) != 0; + boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; + boolean baselineType = (type & TYPE_BASELINE) != 0; + if (shape != null && text && !underline && !strikethrough && baselineType) { + return shape; + } + + Path2D outline = new Path2D(); + BaseTransform tx = new Translate2D(0, 0); + /* Return a shape relative to the baseline of the first line so + * it can be used for layout */ + float firstBaseline = 0; + if (baselineType) { + firstBaseline = -lines[0].getBounds().getMinY(); + } + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + TextRun[] runs = line.getRuns(); + RectBounds bounds = line.getBounds(); + float baseline = -bounds.getMinY(); + for (int j = 0; j < runs.length; j++) { + TextRun run = runs[j]; + FontStrike fontStrike = null; + if (spans != null) { + TextSpan span = run.getTextSpan(); + if (filter != null && span != filter) continue; + PGFont font = (PGFont)span.getFont(); + + /* skip embedded runs */ + if (font == null) continue; + fontStrike = font.getStrike(IDENTITY); + } else { + fontStrike = strike; + } + Point2D location = run.getLocation(); + float runX = location.x; + float runY = location.y + baseline - firstBaseline; + Metrics metrics = null; + if (underline || strikethrough) { + metrics = fontStrike.getMetrics(); + } + if (underline) { + RoundRectangle2D rect = new RoundRectangle2D(); + rect.x = runX; + rect.y = runY + metrics.getUnderLineOffset(); + rect.width = run.getWidth(); + rect.height = metrics.getUnderLineThickness(); + outline.append(rect, false); + } + if (strikethrough) { + RoundRectangle2D rect = new RoundRectangle2D(); + rect.x = runX; + rect.y = runY + metrics.getStrikethroughOffset(); + rect.width = run.getWidth(); + rect.height = metrics.getStrikethroughThickness(); + outline.append(rect, false); + } + if (text && run.getGlyphCount() > 0) { + tx.restoreTransform(1, 0, 0, 1, runX, runY); + Path2D path = (Path2D)fontStrike.getOutline(run, tx); + outline.append(path, false); + } + } + } + + if (text && !underline && !strikethrough) { + shape = outline; + } + return outline; + } + + @Override + public boolean setTabSize(int spaces) { + if (spaces < 1) { + spaces = 1; + } + if (tabSize != spaces) { + tabSize = spaces; + relayout(); + return true; + } + return false; + } + + /*************************************************************************** + * * + * Text Layout Implementation * + * * + **************************************************************************/ + + private int getLineIndex(float y) { + int index = 0; + float bottom = 0; + + int lineCount = getLineCount(); + while (index < lineCount) { + bottom += lines[index].getBounds().getHeight() + spacing; + if (index + 1 == lineCount) { + bottom -= lines[index].getLeading(); + } + if (bottom > y) { + break; + } + index++; + } + return index; + } + + private boolean copyCache() { + int align = flags & ALIGN_MASK; + int boundsType = flags & BOUNDS_MASK; + /* Caching for boundsType == Center, bias towards Modena */ + return wrapWidth != 0 || align != ALIGN_LEFT || boundsType == 0 || isMirrored(); + } + + private void initCache() { + if (cacheKey != null) { + if (layoutCache == null) { + LayoutCache cache = stringCache.get(cacheKey); + if (cache != null && cache.font.equals(font) && Arrays.equals(cache.text, text)) { + layoutCache = cache; + runs = cache.runs; + runCount = cache.runCount; + flags |= cache.analysis; + } + } + if (layoutCache != null) { + if (copyCache()) { + /* This instance has some property that requires it to + * build its own lines (i.e. wrapping width). Thus, only use + * the runs from the cache (and it needs to make a copy + * before using it as they will be modified). + * Note: the copy of the elements in the array happens in + * reuseRuns(). + */ + if (layoutCache.runs == runs) { + runs = new TextRun[runCount]; + System.arraycopy(layoutCache.runs, 0, runs, 0, runCount); + } + } else { + if (layoutCache.lines != null) { + runs = layoutCache.runs; + runCount = layoutCache.runCount; + flags |= layoutCache.analysis; + lines = layoutCache.lines; + layoutWidth = layoutCache.layoutWidth; + layoutHeight = layoutCache.layoutHeight; + float ascent = lines[0].getBounds().getMinY(); + logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, + layoutWidth, layoutHeight + ascent, 0); + } + } + } + } + } + + private int getLineCount() { + return lines.length; + } + + private int getCharCount() { + if (text != null) return text.length; + int count = 0; + for (int i = 0; i < lines.length; i++) { + count += lines[i].getLength(); + } + return count; + } + + public TextSpan[] getTextSpans() { + return spans; + } + + public PGFont getFont() { + return font; + } + + public int getDirection() { + if ((flags & DIRECTION_LTR) != 0) { + return Bidi.DIRECTION_LEFT_TO_RIGHT; + } + if ((flags & DIRECTION_RTL) != 0) { + return Bidi.DIRECTION_RIGHT_TO_LEFT; + } + if ((flags & DIRECTION_DEFAULT_LTR) != 0) { + return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; + } + if ((flags & DIRECTION_DEFAULT_RTL) != 0) { + return Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT; + } + return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; + } + + public void addTextRun(TextRun run) { + if (runCount + 1 > runs.length) { + TextRun[] newRuns = new TextRun[runs.length + 64]; + System.arraycopy(runs, 0, newRuns, 0, runs.length); + runs = newRuns; + } + runs[runCount++] = run; + } + + private void buildRuns(char[] chars) { + runCount = 0; + if (runs == null) { + int count = Math.max(4, Math.min(chars.length / 16, 16)); + runs = new TextRun[count]; + } + GlyphLayout layout = glyphLayout(); + flags = layout.breakRuns(this, chars, flags); + layout.dispose(); + for (int j = runCount; j < runs.length; j++) { + runs[j] = null; + } + } + + private GlyphLayout glyphLayout() { + return layoutSupplier.get(); + } + + private void shape(TextRun run, char[] chars, GlyphLayout layout) { + FontStrike strike; + PGFont font; + if (spans != null) { + if (spans.length == 0) return; + TextSpan span = run.getTextSpan(); + font = (PGFont)span.getFont(); + if (font == null) { + RectBounds bounds = span.getBounds(); + run.setEmbedded(bounds, span.getText().length()); + return; + } + strike = font.getStrike(IDENTITY); + } else { + font = this.font; + strike = this.strike; + } + + /* init metrics for line breaks for empty lines */ + if (run.getAscent() == 0) { + Metrics m = strike.getMetrics(); + + /* The implementation of the center layoutBounds mode is to assure the + * layout has the same number of pixels above and bellow the cap + * height. + */ + if ((flags & BOUNDS_MASK) == BOUNDS_CENTER) { + float ascent = m.getAscent(); + /* Segoe UI has a very large internal leading area, applying the + * center layoutBounds heuristics on it would result in several pixels + * being added to the descent. The final results would be + * overly large and visually unappealing. The fix is to reduce + * the ascent before applying the algorithm. */ + if (font.getFamilyName().equals("Segoe UI")) { + ascent *= 0.80; + } + ascent = (int)(ascent-0.75); + float descent = (int)(m.getDescent()+0.75); + float leading = (int)(m.getLineGap()+0.75); + float capHeight = (int)(m.getCapHeight()+0.75); + float topPadding = -ascent - capHeight; + if (topPadding > descent) { + descent = topPadding; + } else { + ascent += (topPadding - descent); + } + run.setMetrics(ascent, descent, leading); + } else { + run.setMetrics(m.getAscent(), m.getDescent(), m.getLineGap()); + } + } + + if (run.isTab()) return; + if (run.isLinebreak()) return; + if (run.getGlyphCount() > 0) return; + if (run.isComplex()) { + /* Use GlyphLayout to shape complex text */ + layout.layout(run, font, strike, chars); + } else { + FontResource fr = strike.getFontResource(); + int start = run.getStart(); + int length = run.getLength(); + + /* No glyph layout required */ + if (layoutCache == null) { + float fontSize = strike.getSize(); + CharToGlyphMapper mapper = fr.getGlyphMapper(); + + /* The text contains complex and non-complex runs */ + int[] glyphs = new int[length]; + mapper.charsToGlyphs(start, length, chars, glyphs); + float[] positions = new float[(length + 1) << 1]; + float xadvance = 0; + for (int i = 0; i < length; i++) { + float width = fr.getAdvance(glyphs[i], fontSize); + positions[i<<1] = xadvance; + //yadvance always zero + xadvance += width; + } + positions[length<<1] = xadvance; + run.shape(length, glyphs, positions, null); + } else { + + /* The text only contains non-complex runs, all the glyphs and + * advances are stored in the shapeCache */ + if (!layoutCache.valid) { + float fontSize = strike.getSize(); + CharToGlyphMapper mapper = fr.getGlyphMapper(); + mapper.charsToGlyphs(start, length, chars, layoutCache.glyphs, start); + int end = start + length; + float width = 0; + for (int i = start; i < end; i++) { + float adv = fr.getAdvance(layoutCache.glyphs[i], fontSize); + layoutCache.advances[i] = adv; + width += adv; + } + run.setWidth(width); + } + run.shape(length, layoutCache.glyphs, layoutCache.advances); + } + } + } + + private TextLine createLine(int start, int end, int startOffset, float collapsedSpaceWidth) { + int count = end - start + 1; + + assert count > 0 : "number of TextRuns in a TextLine cannot be less than one: " + count; + + TextRun[] lineRuns = new TextRun[count]; + if (start < runCount) { + System.arraycopy(runs, start, lineRuns, 0, count); + } + + /* Recompute line width, height, and length (wrapping) */ + float width = 0, ascent = 0, descent = 0, leading = 0; + int length = 0; + for (int i = 0; i < lineRuns.length; i++) { + TextRun run = lineRuns[i]; + width += run.getWidth(); + ascent = Math.min(ascent, run.getAscent()); + descent = Math.max(descent, run.getDescent()); + leading = Math.max(leading, run.getLeading()); + length += run.getLength(); + } + + width -= collapsedSpaceWidth; + + if (width > layoutWidth) layoutWidth = width; + return new TextLine(startOffset, length, lineRuns, + width, ascent, descent, leading); + } + + /** + * Computes the size of the white space trailing a given run. + * + * @param run the run to compute trailing space width for, cannot be {@code null} + * @return the X size of the white space trailing the run + */ + private float computeTrailingSpaceWidth(TextRun run) { + float trailingSpaceWidth = 0; + char[] chars = getText(); + + /* + * As the loop below exits when encountering a non-white space character, + * testing each trailing glyph in turn for white space is safe, as white + * space is always represented with only a single glyph: + */ + + for (int i = run.getGlyphCount() - 1; i >= 0; i--) { + int textOffset = run.getStart() + run.getCharOffset(i); + + if (!Character.isWhitespace(chars[textOffset])) { + break; + } + + trailingSpaceWidth += run.getAdvance(i); + } + + return trailingSpaceWidth; + } + + private void reorderLine(TextLine line) { + TextRun[] runs = line.getRuns(); + int length = runs.length; + if (length > 0 && runs[length - 1].isLinebreak()) { + length--; + } + if (length < 2) return; + byte[] levels = new byte[length]; + for (int i = 0; i < length; i++) { + levels[i] = runs[i].getLevel(); + } + Bidi.reorderVisually(levels, 0, runs, 0, length); + } + + private char[] getText() { + if (text == null) { + int count = 0; + for (int i = 0; i < spans.length; i++) { + count += spans[i].getText().length(); + } + text = new char[count]; + int offset = 0; + for (int i = 0; i < spans.length; i++) { + String string = spans[i].getText(); + int length = string.length(); + string.getChars(0, length, text, offset); + offset += length; + } + } + return text; + } + + private boolean isSimpleLayout() { + int textAlignment = flags & ALIGN_MASK; + boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; + int mask = FLAGS_HAS_BIDI | FLAGS_HAS_COMPLEX; + return (flags & mask) == 0 && !justify; + } + + private boolean isMirrored() { + boolean mirrored = false; + switch (flags & DIRECTION_MASK) { + case DIRECTION_RTL: mirrored = true; break; + case DIRECTION_LTR: mirrored = false; break; + case DIRECTION_DEFAULT_LTR: + case DIRECTION_DEFAULT_RTL: + mirrored = (flags & FLAGS_RTL_BASE) != 0; + } + return mirrored; + } + + private float getMirroringWidth() { + /* The text node in the scene layer is mirrored based on + * result of computeLayoutBounds. The coordinate translation + * in text layout has to be based on the same width. + */ + return wrapWidth != 0 ? wrapWidth : layoutWidth; + } + + private void reuseRuns() { + /* The runs list is always accessed by the same thread (as TextLayout + * is not thread safe) thus it can be modified at any time, but the + * elements inside of the list are shared among threads and cannot be + * modified. Each reused element has to be cloned.*/ + runCount = 0; + int index = 0; + while (index < runs.length) { + TextRun run = runs[index]; + if (run == null) break; + runs[index] = null; + index++; + runs[runCount++] = run = run.unwrap(); + + if (run.isSplit()) { + run.merge(null); /* unmark split */ + while (index < runs.length) { + TextRun nextRun = runs[index]; + if (nextRun == null) break; + run.merge(nextRun); + runs[index] = null; + index++; + if (nextRun.isSplitLast()) break; + } + } + } + } + + private float getTabAdvance() { + float spaceAdvance = 0; + if (spans != null) { + /* Rich text case - use the first font (for now) */ + for (int i = 0; i < spans.length; i++) { + TextSpan span = spans[i]; + PGFont font = (PGFont)span.getFont(); + if (font != null) { + FontStrike strike = font.getStrike(IDENTITY); + spaceAdvance = strike.getCharAdvance(' '); + break; + } + } + } else { + spaceAdvance = strike.getCharAdvance(' '); + } + return tabSize * spaceAdvance; + } + + /* + * The way JavaFX lays out text: + * + * JavaFX distinguishes between soft wraps and hard wraps. Soft wraps + * occur when a wrap width has been set and the text requires wrapping + * to stay within the set wrap width. Hard wraps are explicitly part of + * the text in the form of line feeds (LF) and carriage returns (CR). + * Hard wrapping considers a singular LF or CR, or the combination of + * CR+LF (or LF+CR) as a single wrap location. Hard wrapping also occurs + * between TextSpans when multiple TextSpans were supplied (for wrapping + * purposes, there is no difference between two TextSpans and a single + * TextSpan where the text was concatenated with a line break in between). + * + * Soft wrapping occurs when a wrap width has been set. This occurs at + * the first character that does not fit. + * + * - If that character is not a white space, the break is set immediately + * after the first white space encountered before that character + * - If there is no white space before the preferred break character, the + * break is done at the first character that does not fit (the wrap + * then occurs in the middle of a (long) word) + * - If the preferred break character is white space, and it is followed by + * more white space, the break is moved to the end of the white space (thus + * a break in white space always occurs at first non white space character + * following a white space sequence) + * + * White space collapsing: + * + * Only white space that is present at soft wrapped locations is collapsed to + * zero. Any other white space is preserved. This includes white space between + * words, leading and trailing white space, and white space around hard wrapped + * locations. + * + * Alignment: + * + * The alignment calculation only looks at the width of all the significant + * characters in each line. Significant characters are any non white space + * characters and any white space that has been preserved (white space that wasn't + * collapsed due to soft wrapping). + * + * Alignment does not take text effects, such as strike through and underline, into + * account. This means that such effects can appear unaligned. Trailing spaces at a + * soft wrap location (that are underlined for example), may show the underline go + * outside the logical bounds of the text. + * + * Example, where indicates a soft wrap location, and is a line feed: + * + * " The quick brown fox jumps over the lazy dog " + * + * Would be rendered as (left aligned): + * + * " The quick" + * "brown fox jumps" + * "over the " + * " lazy dog " + * + * The alignment calculation uses the above bounds indicated by the double + * quotes, and so right aligned text would look like: + * + * " The quick" + * "brown fox jumps" + * "over the " + * " lazy dog " + * + * Note that only the white space at the soft wrap locations is collapsed. + * In all other locations the space was preserved (the space between words + * where no soft wrap occurred, the leading and trailing space, and the + * space around the hard wrapped location). + * + * Text effects have no effect on the alignment, and so with underlining on + * the right aligned text would look like: + * + * "___The___quick_" (one collapsed space becomes visible here) + * "brown_fox_jumps__" (two collapsed spaces become visible here) + * "over_the_" + * "_lazy_dog___" + * + * Note that text alignment has not changed at all, but the bounds are exceeded + * in some locations to allow for the underline. Controls displaying such texts + * will likely clip the underlined parts exceeding the bounds. + * + * Users wishing to mitigate some of these perhaps surprising results can ensure + * they use trimmed texts, and avoid the use of line breaks, or at least ensure + * that line breaks are not preceded or succeeded by white space (activating + * line wrapping is not equivalent to collapsing any consecutive white space + * no matter where it occurs). + */ + + private void layout() { + /* Try the cache */ + initCache(); + + /* Whole layout retrieved from the cache */ + if (lines != null) return; + char[] chars = getText(); + + /* runs and runCount are set in reuseRuns or buildRuns */ + if ((flags & FLAGS_ANALYSIS_VALID) != 0 && isSimpleLayout()) { + reuseRuns(); + } else { + buildRuns(chars); + } + + GlyphLayout layout = null; + if ((flags & (FLAGS_HAS_COMPLEX)) != 0) { + layout = glyphLayout(); + } + + float tabAdvance = 0; + if ((flags & FLAGS_HAS_TABS) != 0) { + tabAdvance = getTabAdvance(); + } + + BreakIterator boundary = null; + if (wrapWidth > 0) { + if ((flags & (FLAGS_HAS_COMPLEX | FLAGS_HAS_CJK)) != 0) { + boundary = BreakIterator.getLineInstance(); + boundary.setText(new CharArrayIterator(chars)); + } + } + int textAlignment = flags & ALIGN_MASK; + + /* Optimize simple case: reuse the glyphs and advances as long as the + * text and font are the same. + * The simple case is no bidi, no complex, no justify, no features. + */ + + if (isSimpleLayout()) { + if (layoutCache == null) { + layoutCache = new LayoutCache(); + layoutCache.glyphs = new int[chars.length]; + layoutCache.advances = new float[chars.length]; + } + } else { + layoutCache = null; + } + + float lineWidth = 0; + int startIndex = 0; + int startOffset = 0; + ArrayList linesList = new ArrayList<>(); + for (int i = 0; i < runCount; i++) { + TextRun run = runs[i]; + shape(run, chars, layout); + if (run.isTab()) { + float tabStop = ((int)(lineWidth / tabAdvance) +1) * tabAdvance; + run.setWidth(tabStop - lineWidth); + } + + float runWidth = run.getWidth(); + if (wrapWidth > 0 && lineWidth + runWidth > wrapWidth && !run.isLinebreak()) { + + /* Find offset of the first character that does not fit on the line */ + int hitOffset = run.getStart() + run.getWrapIndex(wrapWidth - lineWidth); + + /* + * Only keep white spaces (not tabs) in the current run to avoid + * dealing with unshaped runs. + * + * If the run is a tab, the run will be always of length 1 (see + * buildRuns()). As there is no "next" character that can be selected + * as the wrap index in this run, the white space skipping logic + * below won't skip tabs. + */ + + int offset = hitOffset; + int runEnd = run.getEnd(); + + // Don't take white space into account at the preferred wrap index: + while (offset + 1 < runEnd && Character.isWhitespace(chars[offset])) { + offset++; + } + + /* Find the break opportunity */ + int breakOffset = offset; + if (boundary != null) { + /* Use Java BreakIterator when complex script are present */ + breakOffset = boundary.isBoundary(offset) || chars[offset] == '\t' ? offset : boundary.preceding(offset); + } else { + /* Simple break strategy for latin text (Performance) */ + boolean currentChar = Character.isWhitespace(chars[breakOffset]); + while (breakOffset > startOffset) { + boolean previousChar = Character.isWhitespace(chars[breakOffset - 1]); + if (!currentChar && previousChar) break; + currentChar = previousChar; + breakOffset--; + } + } + + /* Never break before the line start offset */ + if (breakOffset < startOffset) breakOffset = startOffset; + + /* Find the run that contains the break offset */ + int breakRunIndex = startIndex; + TextRun breakRun = null; + while (breakRunIndex < runCount) { + breakRun = runs[breakRunIndex]; + if (breakRun.getEnd() > breakOffset) break; + breakRunIndex++; + } + + /* No line breaks between hit offset and line start offset. + * Try character wrapping mode at the hit offset. + */ + if (breakOffset == startOffset) { + breakRun = run; + breakRunIndex = i; + breakOffset = hitOffset; + } + + int breakOffsetInRun = breakOffset - breakRun.getStart(); + /* Wrap the entire run to the next (only if it is not the first + * run of the line). + */ + if (breakOffsetInRun == 0 && breakRunIndex != startIndex) { + i = breakRunIndex - 1; + } else { + i = breakRunIndex; + + /* The break offset is at the first offset of the first run of the line. + * This happens when the wrap width is smaller than the width require + * to show the first character for the line. + */ + if (breakOffsetInRun == 0) { + breakOffsetInRun++; + } + if (breakOffsetInRun < breakRun.getLength()) { + if (runCount >= runs.length) { + TextRun[] newRuns = new TextRun[runs.length + 64]; + System.arraycopy(runs, 0, newRuns, 0, i + 1); + System.arraycopy(runs, i + 1, newRuns, i + 2, runs.length - i - 1); + runs = newRuns; + } else { + System.arraycopy(runs, i + 1, runs, i + 2, runCount - i - 1); + } + runs[i + 1] = breakRun.split(breakOffsetInRun); + if (breakRun.isComplex()) { + shape(breakRun, chars, layout); + } + runCount++; + } + } + + /* No point marking the last run of a line a softbreak */ + if (i + 1 < runCount && !runs[i + 1].isLinebreak()) { + run = runs[i]; + run.setSoftbreak(); + flags |= FLAGS_WRAPPED; + + // Tabs should preserve width + + /* + * Due to contextual forms (arabic) it is possible this line + * is still too big since the splitting of the arabic run + * changes the shape of boundary glyphs. For now the + * implementation has opted to have the appropriate + * initial/final shapes and allow those glyphs to + * potentially overlap the wrapping width, rather than use + * the medial form within the wrappingWidth. A better place + * to solve this would be TextRun#getWrapIndex - but its TBD + * there too. + */ + } + } + + lineWidth += runWidth; + if (run.isBreak()) { + TextLine line = createLine(startIndex, i, startOffset, computeTrailingSpaceWidth(runs[i])); + linesList.add(line); + startIndex = i + 1; + startOffset += line.getLength(); + lineWidth = 0; + } + } + if (layout != null) layout.dispose(); + + linesList.add(createLine(startIndex, runCount - 1, startOffset, 0)); + lines = new TextLine[linesList.size()]; + linesList.toArray(lines); + + float fullWidth = wrapWidth > 0 ? wrapWidth : layoutWidth; // layoutWidth = widest line, wrapWidth is user set + float lineY = 0; + float align; + if (isMirrored()) { + align = 1; /* Left and Justify */ + if (textAlignment == ALIGN_RIGHT) align = 0; + } else { + align = 0; /* Left and Justify */ + if (textAlignment == ALIGN_RIGHT) align = 1; + } + if (textAlignment == ALIGN_CENTER) align = 0.5f; + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + int lineStart = line.getStart(); + RectBounds bounds = line.getBounds(); + + /* Center and right alignment */ + float unusedWidth = fullWidth - bounds.getWidth(); + float lineX = unusedWidth * align; + line.setAlignment(lineX); + + /* Justify */ + boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; + if (justify) { + TextRun[] lineRuns = line.getRuns(); + int lineRunCount = lineRuns.length; + if (lineRunCount > 0 && lineRuns[lineRunCount - 1].isSoftbreak()) { + /* count white spaces but skipping trailings whitespaces */ + int lineEnd = lineStart + line.getLength(); + int wsCount = 0; + boolean hitChar = false; + for (int j = lineEnd - 1; j >= lineStart; j--) { + if (!hitChar && chars[j] != ' ') hitChar = true; + if (hitChar && chars[j] == ' ') wsCount++; + } + if (wsCount != 0) { + float inc = unusedWidth / wsCount; + done: + for (int j = 0; j < lineRunCount; j++) { + TextRun textRun = lineRuns[j]; + int runStart = textRun.getStart(); + int runEnd = textRun.getEnd(); + for (int k = runStart; k < runEnd; k++) { + // TODO kashidas + if (chars[k] == ' ') { + textRun.justify(k - runStart, inc); + if (--wsCount == 0) break done; + } + } + } + lineX = 0; + line.setAlignment(lineX); + line.setWidth(fullWidth); + } + } + } + + if ((flags & FLAGS_HAS_BIDI) != 0) { + reorderLine(line); + } + + computeSideBearings(line); + + /* Set run location */ + float runX = lineX; + TextRun[] lineRuns = line.getRuns(); + for (int j = 0; j < lineRuns.length; j++) { + TextRun run = lineRuns[j]; + run.setLocation(runX, lineY); + run.setLine(line); + runX += run.getWidth(); + } + if (i + 1 < lines.length) { + lineY = Math.max(lineY, lineY + bounds.getHeight() + spacing); + } else { + lineY += (bounds.getHeight() - line.getLeading()); + } + } + float ascent = lines[0].getBounds().getMinY(); + layoutHeight = lineY; + logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, layoutWidth, + layoutHeight + ascent, 0); + + + if (layoutCache != null) { + if (cacheKey != null && !layoutCache.valid && !copyCache()) { + /* After layoutCache is added to the stringCache it can be + * accessed by multiple threads. All the data in it must + * be immutable. See copyCache() for the cases where the entire + * layout is immutable. + */ + layoutCache.font = font; + layoutCache.text = text; + layoutCache.runs = runs; + layoutCache.runCount = runCount; + layoutCache.lines = lines; + layoutCache.layoutWidth = layoutWidth; + layoutCache.layoutHeight = layoutHeight; + layoutCache.analysis = flags & ANALYSIS_MASK; + synchronized (CACHE_SIZE_LOCK) { + int charCount = chars.length; + if (cacheSize + charCount > MAX_CACHE_SIZE) { + stringCache.clear(); + cacheSize = 0; + } + stringCache.put(cacheKey, layoutCache); + cacheSize += charCount; + } + } + layoutCache.valid = true; + } + } + + @Override + public BaseBounds getVisualBounds(int type) { + ensureLayout(); + + /* Not defined for rich text */ + if (strike == null) { + return null; + } + + boolean underline = (type & TYPE_UNDERLINE) != 0; + boolean hasUnderline = (flags & FLAGS_CACHED_UNDERLINE) != 0; + boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; + boolean hasStrikethrough = (flags & FLAGS_CACHED_STRIKETHROUGH) != 0; + if (visualBounds != null && underline == hasUnderline + && strikethrough == hasStrikethrough) { + /* Return last cached value */ + return visualBounds; + } + + flags &= ~(FLAGS_CACHED_STRIKETHROUGH | FLAGS_CACHED_UNDERLINE); + if (underline) flags |= FLAGS_CACHED_UNDERLINE; + if (strikethrough) flags |= FLAGS_CACHED_STRIKETHROUGH; + visualBounds = new RectBounds(); + + float xMin = Float.POSITIVE_INFINITY; + float yMin = Float.POSITIVE_INFINITY; + float xMax = Float.NEGATIVE_INFINITY; + float yMax = Float.NEGATIVE_INFINITY; + float bounds[] = new float[4]; + FontResource fr = strike.getFontResource(); + Metrics metrics = strike.getMetrics(); + float size = strike.getSize(); + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + TextRun[] runs = line.getRuns(); + for (int j = 0; j < runs.length; j++) { + TextRun run = runs[j]; + Point2D pt = run.getLocation(); + if (run.isLinebreak()) continue; + int glyphCount = run.getGlyphCount(); + for (int gi = 0; gi < glyphCount; gi++) { + int gc = run.getGlyphCode(gi); + if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { + fr.getGlyphBoundingBox(run.getGlyphCode(gi), size, bounds); + if (bounds[X_MIN_INDEX] != bounds[X_MAX_INDEX]) { + float glyphX = pt.x + run.getPosX(gi); + float glyphY = pt.y + run.getPosY(gi); + float glyphMinX = glyphX + bounds[X_MIN_INDEX]; + float glyphMinY = glyphY - bounds[Y_MAX_INDEX]; + float glyphMaxX = glyphX + bounds[X_MAX_INDEX]; + float glyphMaxY = glyphY - bounds[Y_MIN_INDEX]; + if (glyphMinX < xMin) xMin = glyphMinX; + if (glyphMinY < yMin) yMin = glyphMinY; + if (glyphMaxX > xMax) xMax = glyphMaxX; + if (glyphMaxY > yMax) yMax = glyphMaxY; + } + } + } + if (underline) { + float underlineMinX = pt.x; + float underlineMinY = pt.y + metrics.getUnderLineOffset(); + float underlineMaxX = underlineMinX + run.getWidth(); + float underlineMaxY = underlineMinY + metrics.getUnderLineThickness(); + if (underlineMinX < xMin) xMin = underlineMinX; + if (underlineMinY < yMin) yMin = underlineMinY; + if (underlineMaxX > xMax) xMax = underlineMaxX; + if (underlineMaxY > yMax) yMax = underlineMaxY; + } + if (strikethrough) { + float strikethroughMinX = pt.x; + float strikethroughMinY = pt.y + metrics.getStrikethroughOffset(); + float strikethroughMaxX = strikethroughMinX + run.getWidth(); + float strikethroughMaxY = strikethroughMinY + metrics.getStrikethroughThickness(); + if (strikethroughMinX < xMin) xMin = strikethroughMinX; + if (strikethroughMinY < yMin) yMin = strikethroughMinY; + if (strikethroughMaxX > xMax) xMax = strikethroughMaxX; + if (strikethroughMaxY > yMax) yMax = strikethroughMaxY; + } + } + } + + if (xMin < xMax && yMin < yMax) { + visualBounds.setBounds(xMin, yMin, xMax, yMax); + } + return visualBounds; + } + + private void computeSideBearings(TextLine line) { + TextRun[] runs = line.getRuns(); + if (runs.length == 0) return; + float bounds[] = new float[4]; + FontResource defaultFontResource = null; + float size = 0; + if (strike != null) { + defaultFontResource = strike.getFontResource(); + size = strike.getSize(); + } + + /* The line lsb is the lsb of the first visual character in the line */ + float lsb = 0; + float width = 0; + lsbdone: + for (int i = 0; i < runs.length; i++) { + TextRun run = runs[i]; + int glyphCount = run.getGlyphCount(); + for (int gi = 0; gi < glyphCount; gi++) { + float advance = run.getAdvance(gi); + /* Skip any leading zero-width glyphs in the line */ + if (advance != 0) { + int gc = run.getGlyphCode(gi); + /* Skip any leading invisible glyphs in the line */ + if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { + FontResource fr = defaultFontResource; + if (fr == null) { + TextSpan span = run.getTextSpan(); + PGFont font = (PGFont)span.getFont(); + /* No need to check font != null (run.glyphCount > 0) */ + size = font.getSize(); + fr = font.getFontResource(); + } + fr.getGlyphBoundingBox(gc, size, bounds); + float glyphLsb = bounds[X_MIN_INDEX]; + lsb = Math.min(0, glyphLsb + width); + run.setLeftBearing(); + break lsbdone; + } + } + width += advance; + } + // tabs + if (glyphCount == 0) { + width += run.getWidth(); + } + } + + /* The line rsb is the rsb of the last visual character in the line */ + float rsb = 0; + width = 0; + rsbdone: + for (int i = runs.length - 1; i >= 0 ; i--) { + TextRun run = runs[i]; + int glyphCount = run.getGlyphCount(); + for (int gi = glyphCount - 1; gi >= 0; gi--) { + float advance = run.getAdvance(gi); + /* Skip any trailing zero-width glyphs in the line */ + if (advance != 0) { + int gc = run.getGlyphCode(gi); + /* Skip any trailing invisible glyphs in the line */ + if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { + FontResource fr = defaultFontResource; + if (fr == null) { + TextSpan span = run.getTextSpan(); + PGFont font = (PGFont)span.getFont(); + /* No need to check font != null (run.glyphCount > 0) */ + size = font.getSize(); + fr = font.getFontResource(); + } + fr.getGlyphBoundingBox(gc, size, bounds); + float glyphRsb = bounds[X_MAX_INDEX] - advance; + rsb = Math.max(0, glyphRsb - width); + run.setRightBearing(); + break rsbdone; + } + } + width += advance; + } + // tabs + if (glyphCount == 0) { + width += run.getWidth(); + } + } + line.setSideBearings(lsb, rsb); + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/TextRun.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/TextRun.java index ed615cf9577..a6a50a9d9fe 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/TextRun.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/TextRun.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -42,7 +42,7 @@ public class TextRun implements GlyphList { byte level; int script; TextSpan span; - TextLine line; + com.sun.javafx.scene.text.TextLine line; Point2D location; private float ascent, descent, leading; int flags = 0; @@ -100,7 +100,7 @@ public byte getLevel() { return line.getBounds(); } - public void setLine(TextLine line) { + public void setLine(com.sun.javafx.scene.text.TextLine line) { this.line = line; } diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontLoader.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontLoader.java index c297bacd563..ca92184737c 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontLoader.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -50,9 +50,7 @@ public class StubFontLoader extends FontLoader { @Override public void loadFont(Font font) { - StubFont stub = new StubFont(); - stub.font = font; - + StubFont stub = new StubFont(font); String name = font.getName(); String nameLower = name.trim().toLowerCase(Locale.ROOT); switch (nameLower) { @@ -201,7 +199,11 @@ public float getSystemFontSize() { } public static class StubFont implements PGFont { - public Font font; + private final Font font; + + public StubFont(Font font) { + this.font = font; + } @Override public String getFullName() { return font.getName(); @@ -225,12 +227,12 @@ public static class StubFont implements PGFont { @Override public FontResource getFontResource() { - return null; + return new StubFontResource(font); } @Override public FontStrike getStrike(BaseTransform transform) { - return null; + return new StubFontStrike(getFontResource(), getSize(), transform); } @Override diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontMetrics.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontMetrics.java new file mode 100644 index 00000000000..e3c0b61c974 --- /dev/null +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontMetrics.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2025, 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.pgstub; + +import com.sun.javafx.font.Metrics; + +/** + * Stubbed FontMetrics, some numbers are still arbitrary. + */ +public class StubFontMetrics implements Metrics { + public static final float BASELINE = 0.8f; + private final float size; + + public StubFontMetrics(float size) { + this.size = size; + } + + @Override + public float getAscent() { + return -size * BASELINE; + } + + @Override + public float getDescent() { + return size * (1.0f - BASELINE); + } + + @Override + public float getLineGap() { + return 0f; + } + + @Override + public float getLineHeight() { + return size; + } + + @Override + public float getTypoAscent() { + return getAscent(); + } + + @Override + public float getTypoDescent() { + return getDescent(); + } + + @Override + public float getTypoLineGap() { + return getLineGap(); + } + + @Override + public float getXHeight() { + return size; + } + + @Override + public float getCapHeight() { + return size; + } + + @Override + public float getStrikethroughOffset() { + return 0; + } + + @Override + public float getStrikethroughThickness() { + return 1; + } + + @Override + public float getUnderLineOffset() { + return 1; + } + + @Override + public float getUnderLineThickness() { + return 1; + } +} diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java new file mode 100644 index 00000000000..9213d6b5409 --- /dev/null +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2025, 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.pgstub; + +import java.lang.ref.WeakReference; +import java.util.Map; +import javafx.scene.text.Font; +import com.sun.javafx.font.CharToGlyphMapper; +import com.sun.javafx.font.FontResource; +import com.sun.javafx.font.FontStrike; +import com.sun.javafx.font.FontStrikeDesc; +import com.sun.javafx.geom.transform.BaseTransform; + +/** + * + */ +public class StubFontResource implements FontResource { + private final Font font; + private static final CharToGlyphMapper glyphMapper = initCharToGlyphMapper(); + + public StubFontResource(Font font) { + this.font = font; + } + + @Override + public String getFullName() { + return font.getName(); + } + + @Override + public String getPSName() { + return font.getName(); + } + + @Override + public String getFamilyName() { + return font.getFamily(); + } + + @Override + public String getFileName() { + return font.getName(); + } + + @Override + public String getStyleName() { + return font.getName(); + } + + @Override + public String getLocaleFullName() { + return getFullName(); + } + + @Override + public String getLocaleFamilyName() { + return getFamilyName(); + } + + @Override + public String getLocaleStyleName() { + return getStyleName(); + } + + // see com.sun.javafx.scene.text.TextLayout flags + @Override + public int getFeatures() { + //StubTextLayout.p(""); + return 0; + } + + @Override + public boolean isBold() { + StubTextLayout.p(""); + // TODO check the name + return false; + } + + @Override + public boolean isItalic() { + return false; + } + + // returns glyph width + @Override + public float getAdvance(int gc, float size) { + //StubTextLayout.p("gx=0x%04X size=%f", gc, size); + return size; + } + + // returns [xmin, ymin, xmax, ymax] + @Override + public float[] getGlyphBoundingBox(int gc, float size, float[] b) { + if (b == null || b.length < 4) { + b = new float[4]; + } + // TODO for non-printable return all 0's + if (gc < 0x20) { + StubTextLayout.p("gc=%04X", gc); + } + + float xmin = 0.0f; + float ymin = 0.0f; + float xmax = size; + float ymax = StubFontMetrics.BASELINE * size; + + // PrismTextLayoutBase: + //private static final int X_MIN_INDEX = 0; + //private static final int Y_MIN_INDEX = 1; + //private static final int X_MAX_INDEX = 2; + //private static final int Y_MAX_INDEX = 3; + b[0] = xmin; + b[1] = ymin; + b[2] = xmax; + b[3] = ymax; + return b; + } + + @Override + public int getDefaultAAMode() { + return 0; + } + + @Override + public CharToGlyphMapper getGlyphMapper() { + return glyphMapper; + } + + @Override + public Map> getStrikeMap() { + StubTextLayout.p(""); + return null; + } + + @Override + public FontStrike getStrike(float size, BaseTransform t) { + return new StubFontStrike(this, size, t); + } + + @Override + public FontStrike getStrike(float size, BaseTransform t, int aaMode) { + return new StubFontStrike(this, size, t); + } + + @Override + public Object getPeer() { + StubTextLayout.p(""); + return null; + } + + @Override + public void setPeer(Object peer) { + StubTextLayout.p(""); + } + + @Override + public boolean isEmbeddedFont() { + return false; + } + + @Override + public boolean isColorGlyph(int gc) { + return false; + } + + private static CharToGlyphMapper initCharToGlyphMapper() { + return new CharToGlyphMapper() { + @Override + public int getGlyphCode(int charCode) { + return charCode; + } + }; + } +} diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java new file mode 100644 index 00000000000..85fa3f3eee6 --- /dev/null +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025, 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.pgstub; + +import com.sun.javafx.font.FontResource; +import com.sun.javafx.font.FontStrike; +import com.sun.javafx.font.Glyph; +import com.sun.javafx.font.Metrics; +import com.sun.javafx.geom.Point2D; +import com.sun.javafx.geom.Shape; +import com.sun.javafx.geom.transform.BaseTransform; +import com.sun.javafx.scene.text.GlyphList; + +/** + * + */ +public class StubFontStrike implements FontStrike { + private final FontResource fontResource; + private final float size; + private final BaseTransform transform; + + public StubFontStrike(FontResource r, float size, BaseTransform t) { + this.fontResource = r; + this.size = size; + this.transform = t; + } + + @Override + public FontResource getFontResource() { + return fontResource; + } + + @Override + public float getSize() { + return size; + } + + @Override + public BaseTransform getTransform() { + return transform; + } + + @Override + public boolean drawAsShapes() { + return false; + } + + @Override + public int getQuantizedPosition(Point2D point) { + point.y = Math.round(point.y); + return 0; + } + + @Override + public Metrics getMetrics() { + return new StubFontMetrics(size); + } + + @Override + public Glyph getGlyph(char symbol) { + StubTextLayout.p(""); + return null; + } + + @Override + public Glyph getGlyph(int glyphCode) { + StubTextLayout.p(""); + return null; + } + + @Override + public void clearDesc() { + StubTextLayout.p(""); + } + + @Override + public int getAAMode() { + return 0; + } + + @Override + public float getCharAdvance(char ch) { + StubTextLayout.p(""); + return 0; + } + + @Override + public Shape getOutline(GlyphList gl, BaseTransform transform) { + StubTextLayout.p(""); + return null; + } +} diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java new file mode 100644 index 00000000000..a5cc47307b3 --- /dev/null +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025, 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.pgstub; + +import com.sun.javafx.font.FontStrike; +import com.sun.javafx.font.PGFont; +import com.sun.javafx.text.GlyphLayout; +import com.sun.javafx.text.TextRun; + +/** + * + */ +public class StubGlyphLayout extends GlyphLayout { + public StubGlyphLayout() { + } + + @Override + public void dispose() { + } + + @Override + public void layout(TextRun run, PGFont font, FontStrike strike, char[] text) { + // TODO + StubTextLayout.p(""); + } +} 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 43f0119ed9e..5d942626020 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 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 @@ -25,641 +25,25 @@ package test.com.sun.javafx.pgstub; -import java.text.BreakIterator; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import javafx.scene.shape.PathElement; -import javafx.scene.text.Font; -import com.sun.javafx.font.CharToGlyphMapper; -import com.sun.javafx.geom.BaseBounds; -import com.sun.javafx.geom.Path2D; -import com.sun.javafx.geom.Point2D; -import com.sun.javafx.geom.RectBounds; -import com.sun.javafx.geom.Shape; -import com.sun.javafx.scene.text.GlyphList; -import com.sun.javafx.scene.text.TextLayout; -import com.sun.javafx.scene.text.TextLine; -import com.sun.javafx.scene.text.TextSpan; +import com.sun.javafx.text.PrismTextLayoutBase; /** - * Stub implementation of the {@link TextLayout} for testing purposes. - *

- * Simulates the text layout by assuming each character is a rectangle - * with the size of the font (bold is 1 pixel wider). - * Expects the text to contain '\n' as line separators. - *

- * This implementation ignores: alignment, bounds type, and direction. + * Same as PrismTextLayout but with stubbed out fonts. */ -public class StubTextLayout implements TextLayout { - private static final boolean DEBUG = false; - private static final double DEFAULT_FONT_SIZE = 10; - private static final float BASELINE = 0.8f; - private TextSpan[] spans; - private String text; - private Font font; - private int tabSize = DEFAULT_TAB_SIZE; - private float lineSpacing; - private float wrapWidth; - private StubTextLine[] lines; +public class StubTextLayout extends PrismTextLayoutBase { + public static final boolean DEBUG = true; public StubTextLayout() { + super(256, StubGlyphLayout::new); } - @Override - public boolean setContent(TextSpan[] spans) { - this.spans = spans; - this.text = null; - lines = null; - return true; - } - - @Override - public boolean setContent(String text, Object font) { - this.text = text; - final StubFontLoader.StubFont stub = ((StubFontLoader.StubFont)font); - this.font = stub == null ? null : stub.font; - lines = null; - return true; - } - - @Override - public boolean setAlignment(int alignment) { - return true; - } - - @Override - public boolean setDirection(int direction) { - return true; - } - - @Override - public boolean setLineSpacing(float spacing) { - this.lineSpacing = spacing; - lines = null; - return true; - } - - @Override - public boolean setTabSize(int spaces) { - if (spaces < 1) { - spaces = 1; - } - if (tabSize != spaces) { - tabSize = spaces; - return true; - } - lines = null; - return false; - } - - @Override - public boolean setWrapWidth(float wrapWidth) { - this.wrapWidth = wrapWidth; - lines = null; - return true; - } - - @Override - public boolean setBoundsType(int type) { - return true; - } - - @Override - public BaseBounds getBounds() { - return getBounds(null, new RectBounds()); - } - - @Override - public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { - ensureLayout(); - // copied from PrismTextLayout: - float left = Float.POSITIVE_INFINITY; - float top = Float.POSITIVE_INFINITY; - float right = Float.NEGATIVE_INFINITY; - float bottom = Float.NEGATIVE_INFINITY; - if (filter != null) { - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - GlyphList[] lineRuns = line.getRuns(); - for (int j = 0; j < lineRuns.length; j++) { - GlyphList run = lineRuns[j]; - TextSpan span = run.getTextSpan(); - if (span != filter) continue; - Point2D location = run.getLocation(); - float runLeft = location.x; - //if (run.isLeftBearing()) { - // runLeft += line.getLeftSideBearing(); - //} - float runRight = location.x + run.getWidth(); - //if (run.isRightBearing()) { - // runRight += line.getRightSideBearing(); - //} - float runTop = location.y; - float runBottom = location.y + line.getBounds().getHeight() + lineSpacing; - if (runLeft < left) left = runLeft; - if (runTop < top) top = runTop; - if (runRight > right) right = runRight; - if (runBottom > bottom) bottom = runBottom; - } - } - } else { - top = bottom = 0; - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - RectBounds lineBounds = line.getBounds(); - float lineLeft = lineBounds.getMinX() + line.getLeftSideBearing(); - if (lineLeft < left) left = lineLeft; - float lineRight = lineBounds.getMaxX() + line.getRightSideBearing(); - if (lineRight > right) right = lineRight; - bottom += lineBounds.getHeight(); - } - //if (isMirrored()) { - // float width = getMirroringWidth(); - // float bearing = left; - // left = width - right; - // right = width - bearing; - //} - } - return bounds.deriveWithNewBounds(left, top, 0, right, bottom, 0); - } - - @Override - public TextLine[] getLines() { - ensureLayout(); - return lines; - } - - @Override - public GlyphList[] getRuns() { - ensureLayout(); - ArrayList rv = new ArrayList<>(); - for (StubTextLine line : lines) { - for (GlyphList g : line.getRuns()) { - rv.add(g); - } - } - return rv.toArray(GlyphList[]::new); - } - - @Override - public Shape getShape(int type, TextSpan filter) { - ensureLayout(); - // all this is undocumented - boolean text = (type & TYPE_TEXT) != 0; - boolean underline = (type & TYPE_UNDERLINE) != 0; - boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; - boolean baselineType = (type & TYPE_BASELINE) != 0; - // TODO - return new Path2D(); - } - - // copied from PrismLayout - // this implementation requires API additions to GlyphList - @Override - public Hit getHitInfo(float x, float y) { - ensureLayout(); - int charIndex = -1; - int insertionIndex = -1; - boolean leading = false; - - ensureLayout(); - int lineIndex = getLineIndex(y); - if (lineIndex >= lines.length) { - charIndex = getCharCount(); - insertionIndex = charIndex + 1; - } else { - TextLine line = lines[lineIndex]; - GlyphList[] runs = line.getRuns(); - RectBounds bounds = line.getBounds(); - GlyphList run = null; - x -= bounds.getMinX(); - 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(); - } - } - if (run != null) { - AtomicBoolean trailing = new AtomicBoolean(); - charIndex = run.getStart() + run.getOffsetAtX(x, trailing); - leading = !trailing.get(); - - insertionIndex = charIndex; - char[] tx = getText(); - if (tx != null && insertionIndex < tx.length) { - if (!leading) { - BreakIterator charIterator = BreakIterator.getCharacterInstance(); - charIterator.setText(new String(tx)); - int next = charIterator.following(insertionIndex); - if (next == BreakIterator.DONE) { - insertionIndex += 1; - } else { - insertionIndex = next; - } - } - } else if (!leading) { - insertionIndex += 1; - } - } else { - //empty line, set to line break leading - charIndex = line.getStart(); - leading = true; - insertionIndex = charIndex; - } - } - return new Hit(charIndex, insertionIndex, leading); - } - - private int getCharCount() { - if (text != null) { - return text.length(); - } - int count = 0; - for (int i = 0; i < lines.length; i++) { - count += lines[i].getLength(); - } - return count; - } - - private int getLineIndex(float y) { - int index = 0; - float bottom = 0; - - int lineCount = lines.length; - while (index < lineCount) { - bottom += lines[index].getBounds().getHeight() + lineSpacing; - //if (index + 1 == lineCount) { - // bottom -= lines[index].getLeading(); - //} - if (bottom > y) { - break; - } - index++; - } - return index; - } - - @Override - public PathElement[] getCaretShape(int offset, boolean isLeading, float x, float y) { - ensureLayout(); - // TODO - return new PathElement[0]; - } - - @Override - public PathElement[] getRange(int start, int end, int type, float x, float y) { - ensureLayout(); - // TODO - return new PathElement[0]; - } - - @Override - public BaseBounds getVisualBounds(int type) { - ensureLayout(); - // TODO - return new RectBounds(); - } - - private char[] getText() { - if (text != null) { - return text.toCharArray(); - } - char[] text; - int count = 0; - for (int i = 0; i < spans.length; i++) { - count += spans[i].getText().length(); - } - text = new char[count]; - int offset = 0; - for (int i = 0; i < spans.length; i++) { - String string = spans[i].getText(); - int length = string.length(); - string.getChars(0, length, text, offset); - offset += length; - } - return text; - } - - private void ensureLayout() { - if (lines == null) { - lines = layout(); - if(DEBUG) { - System.out.println(List.of(lines)); - } - } - } - - private StubTextLine[] layout() { - LayoutBuilder b = new LayoutBuilder(tabSize, lineSpacing, wrapWidth); - if (text != null) { - b.append(null, text, font); - } else if (spans != null) { - for(TextSpan s: spans) { - Object v = s.getFont(); - Font font; - if(v instanceof StubFontLoader.StubFont sf) { - font = sf.font; - } else { - font = (Font)v; - } - b.append(s, s.getText(), font); - } - } - return b.getLines(); - } - - /** Text Line */ - private static class StubTextLine implements TextLine { - private final GlyphList[] runs; - private final RectBounds bounds; - private int start; - private int length; - - public StubTextLine(GlyphList[] runs, RectBounds bounds, int start, int length) { - this.runs = runs; - this.bounds = bounds; - this.start = start; - this.length = length; - } - - @Override - public GlyphList[] getRuns() { - return runs; - } - - @Override - public RectBounds getBounds() { - return bounds; - } - - @Override - public float getLeftSideBearing() { - return 0; - } - - @Override - public float getRightSideBearing() { - return 0; - } - - @Override - public int getStart() { - return start; - } - - @Override - public int getLength() { - return length; - } - - @Override - public String toString() { - return "StubTextLine{" + - "start=" + start + - ", length=" + length + - ", bounds={" + - bounds.getMinX() + - "," + bounds.getMinY() + - " " + bounds.getMaxX() + - "," + bounds.getMaxY() + - "}" + - ", runs=" + List.of(runs) + - "}"; - } - } - - /** Glyph List */ - private static class GlyphRun implements GlyphList { - private final TextSpan span; - private final int start; - private final int length; - private final double x; - private final double y; - private final double charWidth; - private final double charHeight; - private final boolean linebreak; - - public GlyphRun( - TextSpan span, - int start, - int length, - double x, - double y, - double charWidth, - double charHeight, - boolean linebreak - ) { - this.span = span; - this.start = start; - this.length = length; - this.x = x; - this.y = y; - this.charWidth = charWidth; - this.charHeight = charHeight; - this.linebreak = linebreak; - } - - @Override - public String toString() { - return "StubGlyphList{" + - "start=" + start + - ", length=" + length + - ", x=" + x + - ", y=" + y + - "}"; - } - - @Override - public int getStart() { - return start; - } - - @Override - public int getGlyphCount() { - return length; - } - - // this API is rather unclear - @Override - public int getGlyphCode(int glyphIndex) { - // TODO what should it return? for now, let's return the same thing it expects for tab and line break - return CharToGlyphMapper.INVISIBLE_GLYPH_ID; - } - - @Override - public float getPosX(int glyphIndex) { - return (float)(x + glyphIndex * charWidth); - } - - @Override - public float getPosY(int glyphIndex) { - return (float)y; - } - - @Override - public float getWidth() { - return (float)(length * charWidth); - } - - @Override - public float getHeight() { - return (float)charHeight; - } - - @Override - public RectBounds getLineBounds() { - return new RectBounds(0, 0, getWidth(), getHeight()); - } - - @Override - public Point2D getLocation() { - return new Point2D((float)x, (float)y); - } - - @Override - public int getCharOffset(int glyphIndex) { - return start + glyphIndex; - } - - @Override - public boolean isComplex() { - return false; - } - - @Override - public TextSpan getTextSpan() { - return span; - } - - @Override - public boolean isLinebreak() { - return linebreak; - } - - @Override - public int getOffsetAtX(float x, AtomicBoolean trailing) { - double px = Math.max(0.0, x - this.x); - trailing.set(px % charWidth > 0.5); - return (int)(px / charWidth); - } - } - - /** - * Implements a single layout algorithm. - */ - private static class LayoutBuilder { - private final ArrayList lines = new ArrayList<>(); - private final ArrayList runs = new ArrayList<>(); - private final int tabSize; - private final double wrapWidth; - private final double lineSpacing; - private double charHeight; - private double charWidth; - private double x; - private double y; - private double runStartX; - private int runStart; - private int lineStart; - private int column; - private TextSpan span; - private boolean lineBreak; - - public LayoutBuilder(int tabSize, double lineSpacing, double wrapWidth) { - this.tabSize = tabSize; - this.lineSpacing = lineSpacing; - this.wrapWidth = wrapWidth; - } - - public StubTextLine[] getLines() { - if (lines.isEmpty() || !runs.isEmpty()) { - addLine(); - } - return lines.toArray(StubTextLine[]::new); - } - - private void addRun(int offset) { - if (offset > 0) { - GlyphRun r = new GlyphRun(span, runStart, offset, runStartX, y, charWidth, charHeight, lineBreak); - runs.add(r); - runStart = offset; - runStartX = x; - } - lineBreak = false; - } - - private void addLine() { - int len = runStart - lineStart; - GlyphRun[] rs = runs.toArray(GlyphRun[]::new); - runs.clear(); - - float baseline = (float)(charHeight * BASELINE); - RectBounds bounds = new RectBounds(0, -baseline, (float)x, (float)(charHeight) - baseline); - lines.add(new StubTextLine(rs, bounds, lineStart, len)); - - column = 0; - x = 0.0; - runStartX = 0.0; - y += (charHeight + lineSpacing); - lineStart += len; - } - - public void append(TextSpan span, String text, Font f) { - this.span = span; - charHeight = (f == null) ? DEFAULT_FONT_SIZE : f.getSize(); - charWidth = charHeight; - if (f != null) { - boolean bold = f.getStyle().toLowerCase().contains("bold"); - if (bold) { - charWidth++; - } - } - - int len = text.length(); - int i = 0; - for ( ; i < len; i++) { - if (wrapWidth > 0) { - if (x >= wrapWidth) {// FIX >= - addRun(i); - addLine(); - } - } - - char c = text.charAt(i); - switch (c) { - case '\t': - addRun(i); - if (tabSize > 0) { - double dw = (tabSize - (column % tabSize)) * charWidth; - x += dw; - } else { - x += charWidth; - column++; - } - i++; - addRun(i); - break; - case '\n': - lineBreak = true; - addRun(i); - addLine(); - continue; - default: - x += charWidth; - column++; - break; - } - } - - if (i > runStart) { - addRun(i); - } + public static void p(String fmt, Object... args) { + if (DEBUG) { + StackTraceElement s = new Throwable().getStackTrace()[1]; + String name = s.getClassName(); + int ix = name.lastIndexOf('.'); + name = (ix < 0) ? name : name.substring(ix + 1); + System.out.println(name + "." + s.getMethodName() + " " + String.format(fmt, args)); } } } diff --git a/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/BaselineTest.java b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/BaselineTest.java index 4bd438ad22a..15ef48ed8cf 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/BaselineTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/BaselineTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -25,13 +25,13 @@ package test.javafx.scene.layout; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import javafx.scene.Parent; import javafx.scene.ParentShim; import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; - import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests baseline offsets on various classes @@ -51,7 +51,8 @@ public void testShapeBaselineAtBottom() { public void testTextBaseline() { Text text = new Text("Graphically"); float size = (float) text.getFont().getSize(); - assertEquals(size, text.getBaselineOffset(),1e-100); + //assertEquals(size, text.getBaselineOffset(),1e-100); + assertTrue(text.getBaselineOffset() > (size / 2.0f)); } @Test From 8cd8ce3d3d02a8bab87cbd5fa0431c1e2006d098 Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Thu, 23 Jan 2025 14:45:46 -0800 Subject: [PATCH 09/15] add exports --- modules/javafx.controls/src/test/addExports | 2 + .../javafx/scene/chart/LineChartTest.java | 65 +++++++++++-------- .../scene/chart/StackedBarChartTest.java | 4 +- .../javafx/scene/control/AccordionTest.java | 6 +- .../scene/control/ListViewKeyInputTest.java | 8 +-- .../sun/javafx/pgstub/StubFontMetrics.java | 1 + .../sun/javafx/pgstub/StubFontResource.java | 18 +++-- .../com/sun/javafx/pgstub/StubFontStrike.java | 6 +- .../com/sun/javafx/pgstub/StubTextLayout.java | 4 +- 9 files changed, 67 insertions(+), 47 deletions(-) diff --git a/modules/javafx.controls/src/test/addExports b/modules/javafx.controls/src/test/addExports index 8d91f1b3455..4aa75c935e1 100644 --- a/modules/javafx.controls/src/test/addExports +++ b/modules/javafx.controls/src/test/addExports @@ -34,3 +34,5 @@ --add-opens javafx.controls/javafx.scene.control=ALL-UNNAMED # compile additions --add-exports=javafx.graphics/com.sun.javafx.menu=ALL-UNNAMED +--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED + diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/chart/LineChartTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/chart/LineChartTest.java index 1bf064bb812..1397642c378 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/chart/LineChartTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/chart/LineChartTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -188,7 +188,7 @@ public void testPathInsideXAndInsideYBounds() { lineChart.getData().addAll(series1); pulse(); - assertArrayEquals(convertSeriesDataToPoint2D(series1).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(series1, lineChart); } @Test @@ -210,7 +210,7 @@ public void testPathOutsideXBoundsWithDuplicateXAndHigherY() { new XYChart.Data<>(100d, 20d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -232,7 +232,7 @@ public void testPathOutsideXBoundsWithDuplicateXAndLowerY() { new XYChart.Data<>(100d, 20d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -255,7 +255,7 @@ public void testPathOutsideYBoundsWithDuplicateYAndHigherX() { new XYChart.Data<>(90d, 32d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -278,7 +278,7 @@ public void testPathOutsideYBoundsWithDuplicateYAndLowerX() { new XYChart.Data<>(90d, 40d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -300,7 +300,7 @@ public void testPathOutsideXAndYBoundsWithDuplicateXAndHigherY() { new XYChart.Data<>(95d, 35d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -322,7 +322,7 @@ public void testPathOutsideXAndYBoundsWithDuplicateXAndLowerY() { new XYChart.Data<>(95d, 40d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -344,7 +344,7 @@ public void testPathOutsideXAndYBoundsWithDuplicateYAndHigherX() { new XYChart.Data<>(95d, 32d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -366,7 +366,7 @@ public void testPathOutsideXAndYBoundsWithDuplicateYAndLowerX() { new XYChart.Data<>(95d, 40d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -390,7 +390,7 @@ public void testPathOutsideXLowerBoundsWithDuplicateXAndHigherYWithSortYAxis() { new XYChart.Data<>(80d, 10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -414,7 +414,7 @@ public void testPathOutsideXUpperBoundsWithDuplicateXAndHigherYWithSortYAxis() { new XYChart.Data<>(80d, 10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -438,7 +438,7 @@ public void testPathOutsideXLowerBoundsWithDuplicateXAndLowerYWithSortYAxis() { new XYChart.Data<>(80d, 10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -462,7 +462,7 @@ public void testPathOutsideXUpperBoundsWithDuplicateXAndLowerYWithSortYAxis() { new XYChart.Data<>(80d, 10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -485,7 +485,7 @@ public void testPathOutsideYLowerBoundsWithDuplicateYAndHigherXWithSortYAxis() { new XYChart.Data<>(80d, -10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -508,7 +508,7 @@ public void testPathOutsideYUpperBoundsWithDuplicateYAndHigherXWithSortYAxis() { new XYChart.Data<>(80d, 10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -530,7 +530,7 @@ public void testPathOutsideYLowerBoundsWithDuplicateYAndLowerXWithSortYAxis() { new XYChart.Data<>(80d, 10d), new XYChart.Data<>(80d, -10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -553,7 +553,7 @@ public void testPathOutsideYUpperBoundsWithDuplicateYAndLowerXWithSortYAxis() { new XYChart.Data<>(80d, 10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -577,7 +577,7 @@ public void testPathOutsideXAndYLowerBoundsWithDuplicateXAndHigherYWithSortYAxis new XYChart.Data<>(95d, -10d)*/ ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -600,7 +600,7 @@ public void testPathOutsideXAndYUpperBoundsWithDuplicateXAndHigherYWithSortYAxis new XYChart.Data<>(80d, 10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -623,7 +623,7 @@ public void testPathOutsideXAndYLowerBoundsWithDuplicateXAndLowerYWithSortYAxis( new XYChart.Data<>(-10d, -10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -646,7 +646,7 @@ public void testPathOutsideXAndYUpperBoundsWithDuplicateXAndLowerYWithSortYAxis( new XYChart.Data<>(80d, 10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -669,7 +669,7 @@ public void testPathOutsideXAndYLowerBoundsWithDuplicateYAndHigherXWithSortYAxis new XYChart.Data<>(-15d, -10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -692,7 +692,7 @@ public void testPathOutsideXAndYUpperBoundsWithDuplicateYAndHigherXWithSortYAxis new XYChart.Data<>(80d, 10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -715,7 +715,7 @@ public void testPathOutsideXAndYLowerBoundsWithDuplicateYAndLowerXWithSortYAxis( new XYChart.Data<>(-10d, -10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } @Test @@ -738,7 +738,7 @@ public void testPathOutsideXAndYUpperBoundsWithDuplicateYAndLowerXWithSortYAxis( new XYChart.Data<>(80d, 10d) ); - assertArrayEquals(convertSeriesDataToPoint2D(expectedSeries).toArray(), findDataPointsFromPathLine(lineChart).toArray()); + eq(expectedSeries, lineChart); } //JDK-8283675 @@ -777,4 +777,17 @@ private List findDataPointsFromPathLine(LineChart lineC .collect(Collectors.toList()); return data.subList(0, data.size()); } + + private void eq(XYChart.Series expected, LineChart ch) { + List exp = convertSeriesDataToPoint2D(expected); + List res = findDataPointsFromPathLine(lineChart); + + assertEquals(exp.size(), res.size()); + for (int i = 0; i < exp.size(); i++) { + Point2D pe = exp.get(i); + Point2D pr = res.get(i); + assertEquals(pe.getX(), pr.getX(), 1e-9, "at index " + i + " expected=" + pe + " actual=" + pr); + assertEquals(pe.getY(), pr.getY(), 1e-9, "at index " + i + " expected=" + pe + " actual=" + pr); + } + } } diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/chart/StackedBarChartTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/chart/StackedBarChartTest.java index 7d80c885a52..50ccb42e2b7 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/chart/StackedBarChartTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/chart/StackedBarChartTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -113,7 +113,7 @@ public void testSeriesAdd() { // compute bounds for the first series String bounds = computeBoundsString((Region)childrenList.get(0), (Region)childrenList.get(1), (Region)childrenList.get(2)); - assertEquals("10 453 218 35 238 409 218 79 465 355 218 133 ", bounds); + assertEquals("10 440 216 34 236 397 216 77 461 345 216 129 ", bounds); // compute bounds for the second series // bounds = computeBoundsString((Region)childrenList.get(3), (Region)childrenList.get(4), diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/AccordionTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/AccordionTest.java index 7f1d19f8295..a6da1328dd7 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/AccordionTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/AccordionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -185,7 +185,7 @@ private void show() { 0; assertEquals(expectedPrefWidth, accordion.prefWidth(-1), 1e-100); - assertEquals(60, accordion.prefHeight(-1), 1e-100); + assertEquals(78, accordion.prefHeight(-1), 1e-100); accordion.setExpandedPane(b); root.applyCss(); @@ -195,7 +195,7 @@ private void show() { assertEquals(expectedPrefWidth, accordion.prefWidth(-1), 1e-100); final double expectedPrefHeight = PlatformImpl.isCaspian() ? 170 : - PlatformImpl.isModena() ? 161 : + PlatformImpl.isModena() ? 179 : 0; assertEquals(expectedPrefHeight, accordion.prefHeight(-1), 1e-100); } diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewKeyInputTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewKeyInputTest.java index b072df1fffa..fd436071b96 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewKeyInputTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewKeyInputTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -1817,9 +1817,9 @@ private boolean isAnchor(int index) { keyboard.doKeyPress(KeyCode.PAGE_DOWN, KeyModifier.SHIFT); final int leadSelectedIndex = sm.getSelectedIndex(); final int selectedIndicesCount = sm.getSelectedIndices().size(); - assertEquals(6, leadSelectedIndex); - assertEquals(6, fm.getFocusedIndex()); - assertEquals(7, selectedIndicesCount); + assertEquals(4, leadSelectedIndex); + assertEquals(4, fm.getFocusedIndex()); + assertEquals(5, selectedIndicesCount); keyboard.doKeyPress(KeyCode.PAGE_DOWN, KeyModifier.SHIFT); assertEquals(leadSelectedIndex * 2, sm.getSelectedIndex()); diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontMetrics.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontMetrics.java index e3c0b61c974..10727fd7660 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontMetrics.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontMetrics.java @@ -31,6 +31,7 @@ */ public class StubFontMetrics implements Metrics { public static final float BASELINE = 0.8f; + public static final float BOLD_FONT_EXTRA_WIDTH = 1.0f; private final float size; public StubFontMetrics(float size) { diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java index 9213d6b5409..4aa3195cc2a 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java @@ -25,6 +25,7 @@ package test.com.sun.javafx.pgstub; import java.lang.ref.WeakReference; +import java.util.Locale; import java.util.Map; import javafx.scene.text.Font; import com.sun.javafx.font.CharToGlyphMapper; @@ -38,6 +39,7 @@ */ public class StubFontResource implements FontResource { private final Font font; + private Boolean bold; private static final CharToGlyphMapper glyphMapper = initCharToGlyphMapper(); public StubFontResource(Font font) { @@ -87,15 +89,16 @@ public String getLocaleStyleName() { // see com.sun.javafx.scene.text.TextLayout flags @Override public int getFeatures() { - //StubTextLayout.p(""); return 0; } @Override public boolean isBold() { - StubTextLayout.p(""); - // TODO check the name - return false; + if (bold == null) { + String name = font.getStyle(); + bold = name.toLowerCase(Locale.US).contains("bold"); + } + return bold.booleanValue(); } @Override @@ -106,8 +109,9 @@ public boolean isItalic() { // returns glyph width @Override public float getAdvance(int gc, float size) { + // +1 for bold fonts + return isBold() ? size + StubFontMetrics.BOLD_FONT_EXTRA_WIDTH : size; //StubTextLayout.p("gx=0x%04X size=%f", gc, size); - return size; } // returns [xmin, ymin, xmax, ymax] @@ -116,14 +120,14 @@ public float[] getGlyphBoundingBox(int gc, float size, float[] b) { if (b == null || b.length < 4) { b = new float[4]; } - // TODO for non-printable return all 0's + // TODO for non-printable return all 0's? if (gc < 0x20) { StubTextLayout.p("gc=%04X", gc); } float xmin = 0.0f; float ymin = 0.0f; - float xmax = size; + float xmax = getAdvance(gc, size); float ymax = StubFontMetrics.BASELINE * size; // PrismTextLayoutBase: diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java index 85fa3f3eee6..fba00fd4341 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java @@ -69,6 +69,7 @@ public boolean drawAsShapes() { @Override public int getQuantizedPosition(Point2D point) { + point.x = Math.round(point.x); point.y = Math.round(point.y); return 0; } @@ -92,7 +93,6 @@ public Glyph getGlyph(int glyphCode) { @Override public void clearDesc() { - StubTextLayout.p(""); } @Override @@ -102,8 +102,8 @@ public int getAAMode() { @Override public float getCharAdvance(char ch) { - StubTextLayout.p(""); - return 0; + int glyphCode = fontResource.getGlyphMapper().charToGlyph((int)ch); + return fontResource.getAdvance(glyphCode, size); } @Override 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 5d942626020..eb6971a06c5 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -43,7 +43,7 @@ public static void p(String fmt, Object... args) { String name = s.getClassName(); int ix = name.lastIndexOf('.'); name = (ix < 0) ? name : name.substring(ix + 1); - System.out.println(name + "." + s.getMethodName() + " " + String.format(fmt, args)); + System.out.println("🐞 " + name + "." + s.getMethodName() + " " + String.format(fmt, args)); } } } From 6d7682f39bf1ad55e3ada6694b376de43b754483 Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Thu, 23 Jan 2025 15:30:33 -0800 Subject: [PATCH 10/15] more --- .../java/test/javafx/scene/control/ListViewTest.java | 4 ++-- .../javafx/scene/control/TreeViewKeyInputTest.java | 12 ++++++------ .../java/test/javafx/scene/control/TreeViewTest.java | 8 ++++---- modules/javafx.fxml/src/test/addExports | 1 + .../java/test/javafx/scene/text/TextFlowTest.java | 4 ++-- modules/jfx.incubator.richtext/src/test/addExports | 2 ++ 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java index 5aad1351b9e..e0a2c52a458 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -1198,7 +1198,7 @@ public void updateItem(String color, boolean empty) { listView.scrollTo(55); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertEquals(useFixedCellSize ? 17 : 101, rt_35395_counter); + assertEquals(useFixedCellSize ? 17 : 93, rt_35395_counter); sl.dispose(); }); }); diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/TreeViewKeyInputTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/TreeViewKeyInputTest.java index a5ed55239eb..eff1a65556d 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/TreeViewKeyInputTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/TreeViewKeyInputTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -2025,9 +2025,9 @@ private int getItemCount() { keyboard.doKeyPress(KeyCode.PAGE_DOWN, KeyModifier.SHIFT); final int leadSelectedIndex = sm.getSelectedIndex(); final int selectedIndicesCount = sm.getSelectedIndices().size(); - assertEquals(6, leadSelectedIndex); - assertEquals(6, fm.getFocusedIndex()); - assertEquals(7, selectedIndicesCount); + assertEquals(4, leadSelectedIndex); + assertEquals(4, fm.getFocusedIndex()); + assertEquals(5, selectedIndicesCount); keyboard.doKeyPress(KeyCode.PAGE_DOWN, KeyModifier.SHIFT); assertEquals(leadSelectedIndex * 2, sm.getSelectedIndex()); @@ -2068,10 +2068,10 @@ private int getItemCount() { keyboard.doKeyPress(KeyCode.PAGE_UP, KeyModifier.SHIFT); final int leadSelectedIndex = sm.getSelectedIndex(); final int selectedIndicesCount = sm.getSelectedIndices().size(); - final int diff = 4;//99 - leadSelectedIndex; + final int diff = 2;//99 - leadSelectedIndex; assertEquals(99 - diff, leadSelectedIndex); assertEquals(99 - diff, fm.getFocusedIndex()); - assertEquals(5, selectedIndicesCount); + assertEquals(3, selectedIndicesCount); keyboard.doKeyPress(KeyCode.PAGE_UP, KeyModifier.SHIFT); assertEquals(99 - diff * 2 - 1, sm.getSelectedIndex()); diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/TreeViewTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/TreeViewTest.java index 444af2fe49c..7066a3ff628 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/TreeViewTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/TreeViewTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -998,7 +998,7 @@ private void addChildren(TreeItem parent, String name) { // this next test is likely to be brittle, but we'll see...If it is the // cause of failure then it can be commented out // assertEquals(0.125, scrollBar.getVisibleAmount(), 0.0); - assertTrue(scrollBar.getVisibleAmount() > 0.15); + assertTrue(scrollBar.getVisibleAmount() > 0.10); assertTrue(scrollBar.getVisibleAmount() < 0.17); } @@ -1206,14 +1206,14 @@ public void updateItem(String item, boolean empty) { StageLoader sl = new StageLoader(treeView); - assertEquals(24, rt_31200_count); + assertEquals(22, rt_31200_count); // resize the stage sl.getStage().setHeight(250); Toolkit.getToolkit().firePulse(); sl.getStage().setHeight(50); Toolkit.getToolkit().firePulse(); - assertEquals(24, rt_31200_count); + assertEquals(22, rt_31200_count); sl.dispose(); } diff --git a/modules/javafx.fxml/src/test/addExports b/modules/javafx.fxml/src/test/addExports index a63c0b7317d..9ea2354d6c2 100644 --- a/modules/javafx.fxml/src/test/addExports +++ b/modules/javafx.fxml/src/test/addExports @@ -15,3 +15,4 @@ --add-exports javafx.fxml/com.sun.javafx.fxml.expression=ALL-UNNAMED # compilation addons --add-exports javafx.base/com.sun.javafx.beans=ALL-UNNAMED +--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED diff --git a/modules/javafx.graphics/src/test/java/test/javafx/scene/text/TextFlowTest.java b/modules/javafx.graphics/src/test/java/test/javafx/scene/text/TextFlowTest.java index 8f0824e6ff9..cab5618b91d 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/scene/text/TextFlowTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/scene/text/TextFlowTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, 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 @@ -50,7 +50,7 @@ public void testTabSize() { Scene scene = new Scene(root); Stage stage = new Stage(); stage.setScene(scene); - stage.setWidth(300); + stage.setWidth(500); stage.setHeight(200); try { diff --git a/modules/jfx.incubator.richtext/src/test/addExports b/modules/jfx.incubator.richtext/src/test/addExports index bd6ff3e0806..15a326bad1d 100644 --- a/modules/jfx.incubator.richtext/src/test/addExports +++ b/modules/jfx.incubator.richtext/src/test/addExports @@ -18,3 +18,5 @@ # --add-exports jfx.incubator.richtext/com.sun.jfx.incubator.scene.control.dummy=ALL-UNNAMED --add-exports jfx.incubator.richtext/com.sun.jfx.incubator.scene.control.richtext=ALL-UNNAMED +# compile additions +--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED From c8b28bf289a1f242bbc64257c0ce220d886dca0f Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Fri, 24 Jan 2025 11:28:00 -0800 Subject: [PATCH 11/15] magic numbers --- .../javafx/scene/control/ListViewTest.java | 129 ++++++++++-------- 1 file changed, 70 insertions(+), 59 deletions(-) diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java index e0a2c52a458..6923f4d5a1c 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java @@ -703,7 +703,7 @@ public void testSetItemsShouldUpdateTheCells() { // this next test is likely to be brittle, but we'll see...If it is the // cause of failure then it can be commented out // assertEquals(0.125, scrollBar.getVisibleAmount(), 0.0); - assertTrue(scrollBar.getVisibleAmount() > 0.15); + assertTrue(scrollBar.getVisibleAmount() > 0.10); assertTrue(scrollBar.getVisibleAmount() < 0.17); } @@ -826,14 +826,14 @@ public void updateItem(String item, boolean empty) { StageLoader sl = new StageLoader(listView); - assertEquals(24, rt_31200_count); + assertEquals(22, rt_31200_count); // resize the stage sl.getStage().setHeight(250); Toolkit.getToolkit().firePulse(); sl.getStage().setHeight(50); Toolkit.getToolkit().firePulse(); - assertEquals(24, rt_31200_count); + assertEquals(22, rt_31200_count); sl.dispose(); } @@ -2386,10 +2386,14 @@ private void attemptGC(WeakReference weakRef, int n) { @Test public void testUnfixedCellScrollResize() { - final ObservableList items = FXCollections.observableArrayList(300, 300, 70, 20); + int S = 25; + int M = 26; + int L = 70; + final ObservableList items = FXCollections.observableArrayList(300, 300, L, S); final ListView listView = new ListView(items); - listView.setPrefHeight(400); - double viewportLength = 398; // it would be better to calculate this from listView but there is no API for this + double prefHeight = 400; + listView.setPrefHeight(prefHeight); + double viewportLength = toViewPortLength(prefHeight); listView.setCellFactory(lv -> new ListCell<>() { @Override public void updateItem(Integer item, boolean empty) { @@ -2404,50 +2408,53 @@ public void updateItem(Integer item, boolean empty) { listView.scrollTo(2); Toolkit.getToolkit().firePulse(); int cc = VirtualFlowTestUtils.getCellCount(listView); - boolean got70 = false; - boolean got20 = false; + boolean gotLarge = false; + boolean gotSmall = false; for (int i = 0; i < cc; i++) { IndexedCell cell = VirtualFlowTestUtils.getCell(listView, i); - if ((cell != null) && (cell.getItem() == 20)) { - assertEquals(viewportLength - 20, cell.getLayoutY(), 1., "Last cell doesn't end at listview end"); - got20 = true; + if ((cell != null) && (cell.getItem() == S)) { + assertEquals(viewportLength - S, cell.getLayoutY(), 1., "Last cell doesn't end at listview end"); + gotSmall = true; } - if ((cell != null) && (cell.getItem() == 70)) { - assertEquals(viewportLength - 20 - 70, cell.getLayoutY(), 1., "Secondlast cell doesn't end properly"); - got70 = true; + if ((cell != null) && (cell.getItem() == L)) { + assertEquals(viewportLength - S - L, cell.getLayoutY(), 1., "Secondlast cell doesn't end properly"); + gotLarge = true; } } - assertTrue(got20); - assertTrue(got70); + assertTrue(gotSmall); + assertTrue(gotLarge); // resize cells and make sure they align after scrolling ObservableList list = FXCollections.observableArrayList(); - list.addAll(300, 300, 20, 21); + list.addAll(300, 300, S, M); listView.setItems(list); listView.scrollTo(4); Toolkit.getToolkit().firePulse(); - got20 = false; - boolean got21 = false; + gotSmall = false; + boolean gotMedium = false; for (int i = 0; i < cc; i++) { IndexedCell cell = VirtualFlowTestUtils.getCell(listView, i); - if ((cell != null) && (cell.getItem() == 21)) { - assertEquals(viewportLength - 21, cell.getLayoutY(), 1., "Last cell doesn't end at listview end"); - got21 = true; + if ((cell != null) && (cell.getItem() == M)) { + assertEquals(viewportLength - M, cell.getLayoutY(), 1., "Last cell doesn't end at listview end"); + gotMedium = true; } - if ((cell != null) && (cell.getItem() == 20)) { - assertEquals(viewportLength - 21 - 20, cell.getLayoutY(), 1., "Secondlast cell doesn't end properly"); - got20 = true; + if ((cell != null) && (cell.getItem() == S)) { + assertEquals(viewportLength - M - S, cell.getLayoutY(), 1., "Secondlast cell doesn't end properly"); + gotSmall = true; } } - assertTrue(got20); - assertTrue(got21); + assertTrue(gotSmall); + assertTrue(gotMedium); } @Test public void testNoEmptyEnd() { - final ObservableList items = FXCollections.observableArrayList(200, 200, 200, 200, 200, 200, 200, 200, 20, 20, 20, 20, 20, 20, 20); + int S = 25; + int L = 200; + final ObservableList items = FXCollections.observableArrayList(L, L, L, L, L, L, L, L, S, S, S, S, S, S, S); final ListView listView = new ListView(items); - listView.setPrefHeight(400); - double viewportLength = 398; + double prefHeight = 400; + listView.setPrefHeight(prefHeight); + double viewportLength = toViewPortLength(prefHeight); listView.setCellFactory(lv -> new ListCell<>() { @Override public void updateItem(Integer item, boolean empty) { @@ -2463,54 +2470,53 @@ public void updateItem(Integer item, boolean empty) { Toolkit.getToolkit().firePulse(); int cc = VirtualFlowTestUtils.getCellCount(listView); assertEquals(15, cc); - boolean got70 = false; for (int i = 0; i < cc; i++) { IndexedCell cell = VirtualFlowTestUtils.getCell(listView, i); int tens = Math.min(15 - i, 7); int hundreds = Math.max(8 - i, 0); - double exp = 398 - 20 * tens - 200 * hundreds; + double exp = viewportLength - S * tens - L * hundreds; double real = cell.getLayoutY(); if (cell.isVisible()) { - assertEquals(exp, real, 0.1); + assertEquals(exp, real, 0.1, "index=" + i); } } } @Test public void testMoreUnfixedCellScrollResize() { + Integer S = 25; // Sanity Check - it has to work with cases, where all cells have the same sizes - testScrollTo(360, 3, new Integer[]{20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20}); - testScrollTo(360, 3, new Integer[]{20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20}); - testScrollTo(360, 1, new Integer[]{20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20}); - testScrollTo(360, -1, new Integer[]{20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20}); + testScrollTo(360, 3, new Integer[] { S, S, S, S, S, S, S, S, S, S, S, S }); + testScrollTo(360, 3, new Integer[] { S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S }); + testScrollTo(360, 1, new Integer[] { S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S }); + testScrollTo(360, -1, new Integer[] { S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S }); // With 100 it's wrong, when addIncremental is set. testScrollTo(360, 3, new Integer[]{100, 100, 100, 100, 100, 100, 100, 100, 100}); testScrollTo(360, -1, new Integer[]{100, 100, 100, 100, 100, 100, 100, 100, 100}); // More complicated tests - testScrollTo(360, 2, new Integer[]{300, 300, 70, 20}); - testScrollTo(400, 2, new Integer[]{200, 200, 200, 200, 200, 200, 200, 200, 20, 20, 20, 20, 20, 20, 20}); - testScrollTo(400, -1, new Integer[]{200, 200, 200, 200, 200, 200, 200, 200, 20, 20, 20, 20, 20, 20, 20}); - testScrollTo(400, 3, new Integer[]{200, 200, 200, 200, 200, 200, 200, 200, 20, 20, 20, 20, 20, 20, 20}); - testScrollTo(400, -1, new Integer[]{200, 200, 200, 200, 200, 200, 200, 200, 20, 20, 20, 20, 20, 20, 500, 20, 500, 20, 500}); - testScrollTo(400, -1, new Integer[]{200, 200, 200, 200, 200, 200, 200, 200, 20, 20, 20, 20, 20, 20, 400, 20, 400, 20, 400}); - testScrollTo(400, 2, new Integer[]{500, 500, 20, 20, 100, 100, 100, 100, 100, 100}); - testScrollTo(400, 8, new Integer[]{500, 500, 20, 20, 100, 100, 100, 100, 100, 100, 300, 300, 300, 300}); - - testScrollTo(400, 2, new Integer[]{300, 300, 20, 20}); - testScrollTo(400, 2, new Integer[]{300, 300, 20, 20, 200, 200}); - testScrollTo(400, 2, new Integer[]{20, 20, 20, 500, 500}); - - testScrollTo(400, 2, new Integer[]{200, 200, 200, 200, 200, 200, 200, 200, 20, 20, 20, 20, 20, 20, 20}); - testScrollTo(400, -1, new Integer[]{200, 200, 200, 200, 200, 200, 200, 200, 20, 20, 20, 20, 20, 20, 20}); - testScrollTo(400, 3, new Integer[]{200, 200, 200, 200, 200, 200, 200, 200, 20, 20, 20, 20, 20, 20, 20}); - testScrollTo(400, -1, new Integer[]{200, 200, 200, 200, 200, 200, 200, 200, 20, 20, 20, 20, 20, 20, 500, 20, 500, 20, 500}); - testScrollTo(400, -1, new Integer[]{200, 200, 200, 200, 200, 200, 200, 200, 20, 20, 20, 20, 20, 20, 400, 20, 400, 20, 400}); - testScrollTo(400, 2, new Integer[]{500, 500, 20, 20, 100, 100, 100, 100, 100, 100}); - testScrollTo(400, 2, new Integer[]{500, 500, 500, 500, 500}); - + testScrollTo(360, 2, new Integer[] { 300, 300, 70, S }); + testScrollTo(410, 2, new Integer[] { 200, 200, 200, 200, 200, 200, 200, 200, S, S, S, S, S, S, S }); + testScrollTo(400, -1, new Integer[] { 200, 200, 200, 200, 200, 200, 200, 200, S, S, S, S, S, S, S }); + testScrollTo(420, 3, new Integer[] { 200, 200, 200, 200, 200, 200, 200, 200, S, S, S, S, S, S, S }); + testScrollTo(400, -1, new Integer[] { 200, 200, 200, 200, 200, 200, 200, 200, S, S, S, S, S, S, 500, S, 500, S, 500 }); + testScrollTo(400, -1, new Integer[] { 200, 200, 200, 200, 200, 200, 200, 200, S, S, S, S, S, S, 400, S, 400, S, 400 }); + testScrollTo(420, 2, new Integer[] { 500, 500, S, S, 100, 100, 100, 100, 100, 100 }); + testScrollTo(420, 8, new Integer[] { 500, 500, S, S, 100, 100, 100, 100, 100, 100, 300, 300, 300, 300 }); + + testScrollTo(400, 2, new Integer[] { 300, 300, S, S }); + testScrollTo(400, 2, new Integer[] { 300, 300, S, S, 200, 200 }); + testScrollTo(400, 2, new Integer[] { S, S, S, 500, 500 }); + + testScrollTo(400, 2, new Integer[] { 200, 200, 200, 200, 200, 200, 200, 200, S, S, S, S, S, S, S }); + testScrollTo(400, -1, new Integer[] { 200, 200, 200, 200, 200, 200, 200, 200, S, S, S, S, S, S, S }); + testScrollTo(420, 3, new Integer[] { 200, 200, 200, 200, 200, 200, 200, 200, S, S, S, S, S, S, S }); + testScrollTo(400, -1, new Integer[] { 200, 200, 200, 200, 200, 200, 200, 200, S, S, S, S, S, S, 500, S, 500, S, 500 }); + testScrollTo(400, -1, new Integer[] { 200, 200, 200, 200, 200, 200, 200, 200, S, S, S, S, S, S, 400, S, 400, S, 400 }); + testScrollTo(420, 2, new Integer[] { 500, 500, S, S, 100, 100, 100, 100, 100, 100 }); + testScrollTo(400, 2, new Integer[] { 500, 500, 500, 500, 500 }); } public void testScrollTo(int listViewHeight, int scrollToIndex, Integer[] heights) { @@ -2569,7 +2575,7 @@ public void updateItem(Integer item, boolean empty) { public static void verifyListViewScrollTo(ListView listView, int listViewHeight, int scrollToIndex, Integer[] heights) { double sumOfHeights = 0; - double viewportLength = listViewHeight - 2; // it would be better to calculate this from listView but there is no API for this + double viewportLength = toViewPortLength(listViewHeight); for (int height : heights) { sumOfHeights += height; @@ -2665,4 +2671,9 @@ public void fixListViewCrash_JDK_8303680() { // We don't check for the position of the cell, because it's currently don't work properly. // But we wan't to ensure, that the VirtualFlow "Doesn't crash" - which was the case before. } + + private static double toViewPortLength(double prefHeight) { + // it would be better to calculate this from listView but there is no API for this + return prefHeight - 2; + } } From 0f98e4deccda3405899fc852c03d975e993473e1 Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Fri, 24 Jan 2025 11:50:46 -0800 Subject: [PATCH 12/15] more magic --- .../test/javafx/scene/control/ListViewTest.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java index 6923f4d5a1c..2a64f7a7bd6 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java @@ -96,6 +96,12 @@ import test.com.sun.javafx.scene.control.test.Person; import test.com.sun.javafx.scene.control.test.RT_22463_Person; +/** + * NOTE: these tests contain magic numbers which depend on the default font size, + * meaning they are guaranteed to break when/if the default size changes. + * A possible improvement might be to get the default font size and derive the expected + * numbers and preferred sizes. + */ public class ListViewTest { private ListView listView; private MultipleSelectionModel sm; @@ -1148,7 +1154,7 @@ private void test_rt_35395(boolean useFixedCellSize) { ListView listView = new ListView<>(items); if (useFixedCellSize) { - listView.setFixedCellSize(24); + listView.setFixedCellSize(18); } listView.setCellFactory(lv -> new ListCellShim<>() { @Override @@ -1183,12 +1189,12 @@ public void updateItem(String color, boolean empty) { items.remove(12); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertEquals(useFixedCellSize ? 5 : 7, rt_35395_counter); + assertEquals(useFixedCellSize ? 11 : 7, rt_35395_counter); rt_35395_counter = 0; items.add(12, "yellow"); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertEquals(useFixedCellSize ? 5 : 7, rt_35395_counter); + assertEquals(useFixedCellSize ? 11 : 7, rt_35395_counter); rt_35395_counter = 0; listView.scrollTo(5); Platform.runLater(() -> { @@ -1198,7 +1204,7 @@ public void updateItem(String color, boolean empty) { listView.scrollTo(55); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertEquals(useFixedCellSize ? 17 : 93, rt_35395_counter); + assertEquals(useFixedCellSize ? 23 : 93, rt_35395_counter); sl.dispose(); }); }); From f5c91185379a86b9665d088ae01aa70d0ee7c4b7 Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Fri, 24 Jan 2025 13:17:32 -0800 Subject: [PATCH 13/15] cleanup --- .../javafx/scene/control/ListViewTest.java | 9 +- .../font/directwrite/DWGlyphLayout.java | 4 +- .../sun/javafx/font/freetype/FTFactory.java | 6 +- .../com/sun/javafx/sg/prism/NGCanvas.java | 16 +- .../java/com/sun/javafx/text/GlyphLayout.java | 6 +- .../com/sun/javafx/text/PrismTextLayout.java | 1643 +++++++++++++++- .../sun/javafx/text/PrismTextLayoutBase.java | 1674 ----------------- .../javafx/text/PrismTextLayoutFactory.java | 26 +- .../sun/javafx/pgstub/StubFontResource.java | 8 - .../com/sun/javafx/pgstub/StubFontStrike.java | 3 - .../sun/javafx/pgstub/StubGlyphLayout.java | 27 +- .../com/sun/javafx/pgstub/StubTextLayout.java | 18 +- .../com/sun/javafx/text/TextHitInfoTest.java | 7 +- .../com/sun/javafx/text/TextLayoutTest.java | 9 +- 14 files changed, 1718 insertions(+), 1738 deletions(-) delete mode 100644 modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutBase.java diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java index 2a64f7a7bd6..0543867281f 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java @@ -76,6 +76,7 @@ import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; +import javafx.scene.text.Font; import javafx.util.Callback; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -1173,6 +1174,8 @@ public void updateItem(String color, boolean empty) { }); StageLoader sl = new StageLoader(listView); + listView.setPrefHeight(400 / 10.0 * Font.getDefault().getSize()); + Toolkit.getToolkit().firePulse(); Platform.runLater(() -> { rt_35395_counter = 0; @@ -1189,12 +1192,12 @@ public void updateItem(String color, boolean empty) { items.remove(12); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertEquals(useFixedCellSize ? 11 : 7, rt_35395_counter); + assertEquals(useFixedCellSize ? 15 : 10, rt_35395_counter); rt_35395_counter = 0; items.add(12, "yellow"); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertEquals(useFixedCellSize ? 11 : 7, rt_35395_counter); + assertEquals(useFixedCellSize ? 15 : 10, rt_35395_counter); rt_35395_counter = 0; listView.scrollTo(5); Platform.runLater(() -> { @@ -1204,7 +1207,7 @@ public void updateItem(String color, boolean empty) { listView.scrollTo(55); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertEquals(useFixedCellSize ? 23 : 93, rt_35395_counter); + assertEquals(useFixedCellSize ? 27 : 108, rt_35395_counter); sl.dispose(); }); }); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/directwrite/DWGlyphLayout.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/directwrite/DWGlyphLayout.java index 21d7b158eb8..0d015148677 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/directwrite/DWGlyphLayout.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/directwrite/DWGlyphLayout.java @@ -34,7 +34,7 @@ import com.sun.javafx.scene.text.TextSpan; import com.sun.javafx.text.GlyphLayout; import com.sun.javafx.text.GlyphLayoutManager; -import com.sun.javafx.text.PrismTextLayoutBase; +import com.sun.javafx.text.PrismTextLayout; import com.sun.javafx.text.TextRun; public class DWGlyphLayout extends GlyphLayout { @@ -42,7 +42,7 @@ public class DWGlyphLayout extends GlyphLayout { private static final String LOCALE = "en-us"; @Override - protected TextRun addTextRun(PrismTextLayoutBase layout, char[] chars, int start, + protected TextRun addTextRun(PrismTextLayout layout, char[] chars, int start, int length, PGFont font, TextSpan span, byte level) { IDWriteFactory factory = DWFactory.getDWriteFactory(); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/FTFactory.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/FTFactory.java index dedee4cf6b3..fd76cdf10dc 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/FTFactory.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/FTFactory.java @@ -91,7 +91,7 @@ public GlyphLayout createGlyphLayout() { if (OSFreetype.isHarfbuzzEnabled()) { return new HBGlyphLayout(); } - return new StubGlyphLayout(); + return new FTStubGlyphLayout(); } @Override @@ -119,9 +119,9 @@ protected boolean registerEmbeddedFont(String path) { return true; } - private static class StubGlyphLayout extends GlyphLayout { + private static class FTStubGlyphLayout extends GlyphLayout { - public StubGlyphLayout() { + public FTStubGlyphLayout() { } @Override diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/sg/prism/NGCanvas.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/sg/prism/NGCanvas.java index ecbf933d195..3b69f43ea6e 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/sg/prism/NGCanvas.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/sg/prism/NGCanvas.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,12 +25,13 @@ package com.sun.javafx.sg.prism; -import javafx.geometry.VPos; -import javafx.scene.text.Font; import java.nio.IntBuffer; +import java.util.LinkedList; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; -import java.util.LinkedList; +import javafx.geometry.VPos; +import javafx.scene.text.Font; +import javafx.scene.text.FontSmoothingType; import com.sun.javafx.font.PGFont; import com.sun.javafx.geom.Arc2D; import com.sun.javafx.geom.BaseBounds; @@ -46,7 +47,9 @@ import com.sun.javafx.geom.transform.BaseTransform; import com.sun.javafx.geom.transform.NoninvertibleTransformException; import com.sun.javafx.scene.text.FontHelper; +import com.sun.javafx.scene.text.TextLayout; import com.sun.javafx.text.PrismTextLayout; +import com.sun.javafx.text.PrismTextLayoutFactory; import com.sun.javafx.tk.RenderJob; import com.sun.javafx.tk.ScreenConfigurationAccessor; import com.sun.javafx.tk.Toolkit; @@ -72,7 +75,6 @@ import com.sun.scenario.effect.impl.prism.PrDrawable; import com.sun.scenario.effect.impl.prism.PrFilterContext; import com.sun.scenario.effect.impl.prism.PrTexture; -import javafx.scene.text.FontSmoothingType; /** */ @@ -332,7 +334,7 @@ private void restore(Graphics g, int tw, int th) { private BasicStroke stroke; private Path2D path; private NGText ngtext; - private PrismTextLayout textLayout; + private TextLayout textLayout; private PGFont pgfont; private int smoothing; private boolean imageSmoothing; @@ -368,7 +370,7 @@ public NGCanvas() { path = new Path2D(); ngtext = new NGText(); - textLayout = new PrismTextLayout(); + textLayout = PrismTextLayoutFactory.getFactory().createLayout(); transform = new Affine2D(); clipStack = new LinkedList<>(); initAttributes(); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java index 0a816551df8..326865c4616 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/GlyphLayout.java @@ -113,7 +113,7 @@ public abstract class GlyphLayout { } } - protected TextRun addTextRun(PrismTextLayoutBase layout, char[] chars, + protected TextRun addTextRun(PrismTextLayout layout, char[] chars, int start, int length, PGFont font, TextSpan span, byte level) { /* subclass can overwrite this method in order to handle complex text */ @@ -122,7 +122,7 @@ protected TextRun addTextRun(PrismTextLayoutBase layout, char[] chars, return run; } - private TextRun addTextRun(PrismTextLayoutBase layout, char[] chars, + private TextRun addTextRun(PrismTextLayout layout, char[] chars, int start, int length, PGFont font, TextSpan span, byte level, boolean complex) { @@ -139,7 +139,7 @@ private TextRun addTextRun(PrismTextLayoutBase layout, char[] chars, return run; } - public int breakRuns(PrismTextLayoutBase layout, char[] chars, int flags) { + public int breakRuns(PrismTextLayout layout, char[] chars, int flags) { int length = chars.length; boolean complex = false; boolean feature = false; 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 62b762865c3..85e9078d88b 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 @@ -25,10 +25,1645 @@ package com.sun.javafx.text; -import com.sun.javafx.font.PrismFontFactory; +import java.text.Bidi; +import java.text.BreakIterator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Hashtable; +import java.util.concurrent.atomic.AtomicBoolean; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.PathElement; +import com.sun.javafx.font.CharToGlyphMapper; +import com.sun.javafx.font.FontResource; +import com.sun.javafx.font.FontStrike; +import com.sun.javafx.font.Metrics; +import com.sun.javafx.font.PGFont; +import com.sun.javafx.geom.BaseBounds; +import com.sun.javafx.geom.Path2D; +import com.sun.javafx.geom.Point2D; +import com.sun.javafx.geom.RectBounds; +import com.sun.javafx.geom.RoundRectangle2D; +import com.sun.javafx.geom.Shape; +import com.sun.javafx.geom.transform.BaseTransform; +import com.sun.javafx.geom.transform.Translate2D; +import com.sun.javafx.scene.text.GlyphList; +import com.sun.javafx.scene.text.TextLayout; +import com.sun.javafx.scene.text.TextSpan; -public class PrismTextLayout extends PrismTextLayoutBase { - public PrismTextLayout() { - super(PrismFontFactory.cacheLayoutSize, GlyphLayoutManager::getInstance); +/** + * Prism TextLayout + */ +public abstract class PrismTextLayout implements TextLayout { + private static final BaseTransform IDENTITY = BaseTransform.IDENTITY_TRANSFORM; + private static final int X_MIN_INDEX = 0; + private static final int Y_MIN_INDEX = 1; + private static final int X_MAX_INDEX = 2; + private static final int Y_MAX_INDEX = 3; + + private static final Hashtable stringCache = new Hashtable<>(); + private static final Object CACHE_SIZE_LOCK = new Object(); + private static int cacheSize = 0; + private static final int MAX_STRING_SIZE = 256; + private final int MAX_CACHE_SIZE; + + private char[] text; + private TextSpan[] spans; /* Rich text (null for single font text) */ + private PGFont font; /* Single font text (null for rich text) */ + private FontStrike strike; /* cached strike of font (identity) */ + private Integer cacheKey; + private TextLine[] lines; + private TextRun[] runs; + private int runCount; + private BaseBounds logicalBounds; + private RectBounds visualBounds; + private float layoutWidth, layoutHeight; + private float wrapWidth, spacing; + private LayoutCache layoutCache; + private Shape shape; + private int flags; + private int tabSize = DEFAULT_TAB_SIZE; + + public PrismTextLayout(int maxCacheSize) { + MAX_CACHE_SIZE = maxCacheSize; + logicalBounds = new RectBounds(); + flags = ALIGN_LEFT; + } + + private void reset() { + layoutCache = null; + runs = null; + flags &= ~ANALYSIS_MASK; + relayout(); + } + + private void relayout() { + logicalBounds.makeEmpty(); + visualBounds = null; + layoutWidth = layoutHeight = 0; + flags &= ~(FLAGS_WRAPPED | FLAGS_CACHED_UNDERLINE | FLAGS_CACHED_STRIKETHROUGH); + lines = null; + shape = null; + } + + /*************************************************************************** + * * + * TextLayout API * + * * + **************************************************************************/ + + @Override + public boolean setContent(TextSpan[] spans) { + if (spans == null && this.spans == null) return false; + if (spans != null && this.spans != null) { + if (spans.length == this.spans.length) { + int i = 0; + while (i < spans.length) { + if (spans[i] != this.spans[i]) break; + i++; + } + if (i == spans.length) return false; + } + } + + reset(); + this.spans = spans; + this.font = null; + this.strike = null; + this.text = null; /* Initialized in getText() */ + this.cacheKey = null; + return true; + } + + @Override + public boolean setContent(String text, Object font) { + reset(); + this.spans = null; + this.font = (PGFont)font; + this.strike = ((PGFont)font).getStrike(IDENTITY); + this.text = text.toCharArray(); + if (MAX_CACHE_SIZE > 0) { + int length = text.length(); + if (0 < length && length <= MAX_STRING_SIZE) { + cacheKey = text.hashCode() * strike.hashCode(); + } + } + return true; + } + + @Override + public boolean setDirection(int direction) { + if ((flags & DIRECTION_MASK) == direction) return false; + flags &= ~DIRECTION_MASK; + flags |= (direction & DIRECTION_MASK); + reset(); + return true; + } + + @Override + public boolean setBoundsType(int type) { + if ((flags & BOUNDS_MASK) == type) return false; + flags &= ~BOUNDS_MASK; + flags |= (type & BOUNDS_MASK); + reset(); + return true; + } + + @Override + public boolean setAlignment(int alignment) { + int align = ALIGN_LEFT; + switch (alignment) { + case 0: align = ALIGN_LEFT; break; + case 1: align = ALIGN_CENTER; break; + case 2: align = ALIGN_RIGHT; break; + case 3: align = ALIGN_JUSTIFY; break; + } + if ((flags & ALIGN_MASK) == align) return false; + if (align == ALIGN_JUSTIFY || (flags & ALIGN_JUSTIFY) != 0) { + reset(); + } + flags &= ~ALIGN_MASK; + flags |= align; + relayout(); + return true; + } + + @Override + public boolean setWrapWidth(float newWidth) { + if (Float.isInfinite(newWidth)) newWidth = 0; + if (Float.isNaN(newWidth)) newWidth = 0; + float oldWidth = this.wrapWidth; + this.wrapWidth = Math.max(0, newWidth); + + boolean needsLayout = true; + if (lines != null && oldWidth != 0 && newWidth != 0) { + if ((flags & ALIGN_LEFT) != 0) { + if (newWidth > oldWidth) { + /* If wrapping width is increasing and there is no + * wrapped lines then the text remains valid. + */ + if ((flags & FLAGS_WRAPPED) == 0) { + needsLayout = false; + } + } else { + /* If wrapping width is decreasing but it is still + * greater than the max line width then the text + * remains valid. + */ + if (newWidth >= layoutWidth) { + needsLayout = false; + } + } + } + } + if (needsLayout) relayout(); + return needsLayout; + } + + @Override + public boolean setLineSpacing(float spacing) { + if (this.spacing == spacing) return false; + this.spacing = spacing; + relayout(); + return true; + } + + private void ensureLayout() { + if (lines == null) { + layout(); + } + } + + @Override + public com.sun.javafx.scene.text.TextLine[] getLines() { + ensureLayout(); + return lines; + } + + @Override + public GlyphList[] getRuns() { + ensureLayout(); + GlyphList[] result = new GlyphList[runCount]; + int count = 0; + for (int i = 0; i < lines.length; i++) { + GlyphList[] lineRuns = lines[i].getRuns(); + int length = lineRuns.length; + System.arraycopy(lineRuns, 0, result, count, length); + count += length; + } + return result; + } + + @Override + public BaseBounds getBounds() { + ensureLayout(); + return logicalBounds; + } + + @Override + public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { + ensureLayout(); + float left = Float.POSITIVE_INFINITY; + float top = Float.POSITIVE_INFINITY; + float right = Float.NEGATIVE_INFINITY; + float bottom = Float.NEGATIVE_INFINITY; + if (filter != null) { + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + TextRun[] lineRuns = line.getRuns(); + for (int j = 0; j < lineRuns.length; j++) { + TextRun run = lineRuns[j]; + TextSpan span = run.getTextSpan(); + if (span != filter) continue; + Point2D location = run.getLocation(); + float runLeft = location.x; + if (run.isLeftBearing()) { + runLeft += line.getLeftSideBearing(); + } + float runRight = location.x + run.getWidth(); + if (run.isRightBearing()) { + runRight += line.getRightSideBearing(); + } + float runTop = location.y; + float runBottom = location.y + line.getBounds().getHeight() + spacing; + if (runLeft < left) left = runLeft; + if (runTop < top) top = runTop; + if (runRight > right) right = runRight; + if (runBottom > bottom) bottom = runBottom; + } + } + } else { + top = bottom = 0; + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + RectBounds lineBounds = line.getBounds(); + float lineLeft = lineBounds.getMinX() + line.getLeftSideBearing(); + if (lineLeft < left) left = lineLeft; + float lineRight = lineBounds.getMaxX() + line.getRightSideBearing(); + if (lineRight > right) right = lineRight; + bottom += lineBounds.getHeight(); + } + if (isMirrored()) { + float width = getMirroringWidth(); + float bearing = left; + left = width - right; + right = width - bearing; + } + } + return bounds.deriveWithNewBounds(left, top, 0, right, bottom, 0); + } + + @Override + public PathElement[] getCaretShape(int offset, boolean isLeading, + float x, float y) { + ensureLayout(); + int lineIndex = 0; + int lineCount = getLineCount(); + while (lineIndex < lineCount - 1) { + TextLine line = lines[lineIndex]; + int lineEnd = line.getStart() + line.getLength(); + if (lineEnd > offset) break; + lineIndex++; + } + int splitCaretOffset = -1; + int level = 0; + float lineX = 0, lineY = 0, lineHeight = 0; + TextLine line = lines[lineIndex]; + TextRun[] runs = line.getRuns(); + int runCount = runs.length; + int runIndex = -1; + for (int i = 0; i < runCount; i++) { + TextRun run = runs[i]; + int runStart = run.getStart(); + int runEnd = run.getEnd(); + if (runStart <= offset && offset < runEnd) { + if (!run.isLinebreak()) { + runIndex = i; + } + break; + } + } + if (runIndex != -1) { + TextRun run = runs[runIndex]; + int runStart = run.getStart(); + Point2D location = run.getLocation(); + lineX = location.x + run.getXAtOffset(offset - runStart, isLeading); + lineY = location.y; + lineHeight = line.getBounds().getHeight(); + + if (isLeading) { + if (runIndex > 0 && offset == runStart) { + level = run.getLevel(); + splitCaretOffset = offset - 1; + } + } else { + int runEnd = run.getEnd(); + if (runIndex + 1 < runs.length && offset + 1 == runEnd) { + level = run.getLevel(); + splitCaretOffset = offset + 1; + } + } + } else { + /* end of line (line break or offset>=charCount) */ + int maxOffset = 0; + + /* set run index to zero to handle empty line case (only break line) */ + runIndex = 0; + for (int i = 0; i < runCount; i++) { + TextRun run = runs[i]; + /*use the trailing edge of the last logical run*/ + if (run.getStart() >= maxOffset && !run.isLinebreak()) { + maxOffset = run.getStart(); + runIndex = i; + } + } + TextRun run = runs[runIndex]; + Point2D location = run.getLocation(); + lineX = location.x + (run.isLeftToRight() ? run.getWidth() : 0); + lineY = location.y; + lineHeight = line.getBounds().getHeight(); + } + if (isMirrored()) { + lineX = getMirroringWidth() - lineX; + } + lineX += x; + lineY += y; + if (splitCaretOffset != -1) { + for (int i = 0; i < runs.length; i++) { + TextRun run = runs[i]; + int runStart = run.getStart(); + int runEnd = run.getEnd(); + if (runStart <= splitCaretOffset && splitCaretOffset < runEnd) { + if ((run.getLevel() & 1) != (level & 1)) { + Point2D location = run.getLocation(); + float lineX2 = location.x; + if (isLeading) { + if ((level & 1) != 0) lineX2 += run.getWidth(); + } else { + if ((level & 1) == 0) lineX2 += run.getWidth(); + } + if (isMirrored()) { + lineX2 = getMirroringWidth() - lineX2; + } + lineX2 += x; + PathElement[] result = new PathElement[4]; + result[0] = new MoveTo(lineX, lineY); + result[1] = new LineTo(lineX, lineY + lineHeight / 2); + result[2] = new MoveTo(lineX2, lineY + lineHeight / 2); + result[3] = new LineTo(lineX2, lineY + lineHeight); + return result; + } + } + } + } + PathElement[] result = new PathElement[2]; + result[0] = new MoveTo(lineX, lineY); + result[1] = new LineTo(lineX, lineY + lineHeight); + return result; + } + + @Override + public Hit getHitInfo(float x, float y) { + int charIndex = -1; + int insertionIndex = -1; + boolean leading = false; + + ensureLayout(); + int lineIndex = getLineIndex(y); + if (lineIndex >= getLineCount()) { + charIndex = getCharCount(); + insertionIndex = charIndex + 1; + } else { + TextLine line = lines[lineIndex]; + TextRun[] runs = line.getRuns(); + RectBounds bounds = line.getBounds(); + TextRun run = null; + x -= bounds.getMinX(); + 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(); + } + } + if (run != null) { + AtomicBoolean trailing = new AtomicBoolean(); + charIndex = run.getStart() + run.getOffsetAtX(x, trailing); + leading = !trailing.get(); + + insertionIndex = charIndex; + if (getText() != null && insertionIndex < getText().length) { + if (!leading) { + BreakIterator charIterator = BreakIterator.getCharacterInstance(); + charIterator.setText(new String(getText())); + int next = charIterator.following(insertionIndex); + if (next == BreakIterator.DONE) { + insertionIndex += 1; + } else { + insertionIndex = next; + } + } + } else if (!leading) { + insertionIndex += 1; + } + } else { + //empty line, set to line break leading + charIndex = line.getStart(); + leading = true; + insertionIndex = charIndex; + } + } + return new Hit(charIndex, insertionIndex, leading); + } + + @Override + public PathElement[] getRange(int start, int end, int type, + float x, float y) { + ensureLayout(); + int lineCount = getLineCount(); + ArrayList result = new ArrayList<>(); + float lineY = 0; + + for (int lineIndex = 0; lineIndex < lineCount; lineIndex++) { + TextLine line = lines[lineIndex]; + RectBounds lineBounds = line.getBounds(); + int lineStart = line.getStart(); + if (lineStart >= end) break; + int lineEnd = lineStart + line.getLength(); + if (start > lineEnd) { + lineY += lineBounds.getHeight() + spacing; + continue; + } + + /* The list of runs in the line is visually ordered. + * Thus, finding the run that includes the selection end offset + * does not mean that all selected runs have being visited. + * Instead, this implementation first computes the number of selected + * characters in the current line, then iterates over the runs consuming + * selected characters till all of them are found. + */ + TextRun[] runs = line.getRuns(); + int count = Math.min(lineEnd, end) - Math.max(lineStart, start); + int runIndex = 0; + float left = -1; + float right = -1; + float lineX = lineBounds.getMinX(); + while (count > 0 && runIndex < runs.length) { + TextRun run = runs[runIndex]; + int runStart = run.getStart(); + int runEnd = run.getEnd(); + float runWidth = run.getWidth(); + int clmapStart = Math.max(runStart, Math.min(start, runEnd)); + int clampEnd = Math.max(runStart, Math.min(end, runEnd)); + int runCount = clampEnd - clmapStart; + if (runCount != 0) { + boolean ltr = run.isLeftToRight(); + float runLeft; + if (runStart > start) { + runLeft = ltr ? lineX : lineX + runWidth; + } else { + runLeft = lineX + run.getXAtOffset(start - runStart, true); + } + float runRight; + if (runEnd < end) { + runRight = ltr ? lineX + runWidth : lineX; + } else { + runRight = lineX + run.getXAtOffset(end - runStart, true); + } + if (runLeft > runRight) { + float tmp = runLeft; + runLeft = runRight; + runRight = tmp; + } + count -= runCount; + float top = 0, bottom = 0; + switch (type) { + case TYPE_TEXT: + top = lineY; + bottom = lineY + lineBounds.getHeight(); + break; + case TYPE_UNDERLINE: + case TYPE_STRIKETHROUGH: + FontStrike fontStrike = null; + if (spans != null) { + TextSpan span = run.getTextSpan(); + PGFont font = (PGFont)span.getFont(); + if (font == null) break; + fontStrike = font.getStrike(IDENTITY); + } else { + fontStrike = strike; + } + top = lineY - run.getAscent(); + Metrics metrics = fontStrike.getMetrics(); + if (type == TYPE_UNDERLINE) { + top += metrics.getUnderLineOffset(); + bottom = top + metrics.getUnderLineThickness(); + } else { + top += metrics.getStrikethroughOffset(); + bottom = top + metrics.getStrikethroughThickness(); + } + break; + } + + /* Merge continuous rectangles */ + if (runLeft != right) { + if (left != -1 && right != -1) { + float l = left, r = right; + if (isMirrored()) { + float width = getMirroringWidth(); + l = width - l; + r = width - r; + } + result.add(new MoveTo(x + l, y + top)); + result.add(new LineTo(x + r, y + top)); + result.add(new LineTo(x + r, y + bottom)); + result.add(new LineTo(x + l, y + bottom)); + result.add(new LineTo(x + l, y + top)); + } + left = runLeft; + right = runRight; + } + right = runRight; + if (count == 0) { + float l = left, r = right; + if (isMirrored()) { + float width = getMirroringWidth(); + l = width - l; + r = width - r; + } + result.add(new MoveTo(x + l, y + top)); + result.add(new LineTo(x + r, y + top)); + result.add(new LineTo(x + r, y + bottom)); + result.add(new LineTo(x + l, y + bottom)); + result.add(new LineTo(x + l, y + top)); + } + } + lineX += runWidth; + runIndex++; + } + lineY += lineBounds.getHeight() + spacing; + } + return result.toArray(new PathElement[result.size()]); + } + + @Override + public Shape getShape(int type, TextSpan filter) { + ensureLayout(); + boolean text = (type & TYPE_TEXT) != 0; + boolean underline = (type & TYPE_UNDERLINE) != 0; + boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; + boolean baselineType = (type & TYPE_BASELINE) != 0; + if (shape != null && text && !underline && !strikethrough && baselineType) { + return shape; + } + + Path2D outline = new Path2D(); + BaseTransform tx = new Translate2D(0, 0); + /* Return a shape relative to the baseline of the first line so + * it can be used for layout */ + float firstBaseline = 0; + if (baselineType) { + firstBaseline = -lines[0].getBounds().getMinY(); + } + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + TextRun[] runs = line.getRuns(); + RectBounds bounds = line.getBounds(); + float baseline = -bounds.getMinY(); + for (int j = 0; j < runs.length; j++) { + TextRun run = runs[j]; + FontStrike fontStrike = null; + if (spans != null) { + TextSpan span = run.getTextSpan(); + if (filter != null && span != filter) continue; + PGFont font = (PGFont)span.getFont(); + + /* skip embedded runs */ + if (font == null) continue; + fontStrike = font.getStrike(IDENTITY); + } else { + fontStrike = strike; + } + Point2D location = run.getLocation(); + float runX = location.x; + float runY = location.y + baseline - firstBaseline; + Metrics metrics = null; + if (underline || strikethrough) { + metrics = fontStrike.getMetrics(); + } + if (underline) { + RoundRectangle2D rect = new RoundRectangle2D(); + rect.x = runX; + rect.y = runY + metrics.getUnderLineOffset(); + rect.width = run.getWidth(); + rect.height = metrics.getUnderLineThickness(); + outline.append(rect, false); + } + if (strikethrough) { + RoundRectangle2D rect = new RoundRectangle2D(); + rect.x = runX; + rect.y = runY + metrics.getStrikethroughOffset(); + rect.width = run.getWidth(); + rect.height = metrics.getStrikethroughThickness(); + outline.append(rect, false); + } + if (text && run.getGlyphCount() > 0) { + tx.restoreTransform(1, 0, 0, 1, runX, runY); + Path2D path = (Path2D)fontStrike.getOutline(run, tx); + outline.append(path, false); + } + } + } + + if (text && !underline && !strikethrough) { + shape = outline; + } + return outline; + } + + @Override + public boolean setTabSize(int spaces) { + if (spaces < 1) { + spaces = 1; + } + if (tabSize != spaces) { + tabSize = spaces; + relayout(); + return true; + } + return false; + } + + /*************************************************************************** + * * + * Text Layout Implementation * + * * + **************************************************************************/ + + private int getLineIndex(float y) { + int index = 0; + float bottom = 0; + + int lineCount = getLineCount(); + while (index < lineCount) { + bottom += lines[index].getBounds().getHeight() + spacing; + if (index + 1 == lineCount) { + bottom -= lines[index].getLeading(); + } + if (bottom > y) { + break; + } + index++; + } + return index; + } + + private boolean copyCache() { + int align = flags & ALIGN_MASK; + int boundsType = flags & BOUNDS_MASK; + /* Caching for boundsType == Center, bias towards Modena */ + return wrapWidth != 0 || align != ALIGN_LEFT || boundsType == 0 || isMirrored(); + } + + private void initCache() { + if (cacheKey != null) { + if (layoutCache == null) { + LayoutCache cache = stringCache.get(cacheKey); + if (cache != null && cache.font.equals(font) && Arrays.equals(cache.text, text)) { + layoutCache = cache; + runs = cache.runs; + runCount = cache.runCount; + flags |= cache.analysis; + } + } + if (layoutCache != null) { + if (copyCache()) { + /* This instance has some property that requires it to + * build its own lines (i.e. wrapping width). Thus, only use + * the runs from the cache (and it needs to make a copy + * before using it as they will be modified). + * Note: the copy of the elements in the array happens in + * reuseRuns(). + */ + if (layoutCache.runs == runs) { + runs = new TextRun[runCount]; + System.arraycopy(layoutCache.runs, 0, runs, 0, runCount); + } + } else { + if (layoutCache.lines != null) { + runs = layoutCache.runs; + runCount = layoutCache.runCount; + flags |= layoutCache.analysis; + lines = layoutCache.lines; + layoutWidth = layoutCache.layoutWidth; + layoutHeight = layoutCache.layoutHeight; + float ascent = lines[0].getBounds().getMinY(); + logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, + layoutWidth, layoutHeight + ascent, 0); + } + } + } + } + } + + private int getLineCount() { + return lines.length; + } + + private int getCharCount() { + if (text != null) return text.length; + int count = 0; + for (int i = 0; i < lines.length; i++) { + count += lines[i].getLength(); + } + return count; + } + + public TextSpan[] getTextSpans() { + return spans; + } + + public PGFont getFont() { + return font; + } + + public int getDirection() { + if ((flags & DIRECTION_LTR) != 0) { + return Bidi.DIRECTION_LEFT_TO_RIGHT; + } + if ((flags & DIRECTION_RTL) != 0) { + return Bidi.DIRECTION_RIGHT_TO_LEFT; + } + if ((flags & DIRECTION_DEFAULT_LTR) != 0) { + return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; + } + if ((flags & DIRECTION_DEFAULT_RTL) != 0) { + return Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT; + } + return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; + } + + public void addTextRun(TextRun run) { + if (runCount + 1 > runs.length) { + TextRun[] newRuns = new TextRun[runs.length + 64]; + System.arraycopy(runs, 0, newRuns, 0, runs.length); + runs = newRuns; + } + runs[runCount++] = run; + } + + private void buildRuns(char[] chars) { + runCount = 0; + if (runs == null) { + int count = Math.max(4, Math.min(chars.length / 16, 16)); + runs = new TextRun[count]; + } + GlyphLayout layout = glyphLayout(); + flags = layout.breakRuns(this, chars, flags); + layout.dispose(); + for (int j = runCount; j < runs.length; j++) { + runs[j] = null; + } + } + + protected abstract GlyphLayout glyphLayout(); + + private void shape(TextRun run, char[] chars, GlyphLayout layout) { + FontStrike strike; + PGFont font; + if (spans != null) { + if (spans.length == 0) return; + TextSpan span = run.getTextSpan(); + font = (PGFont)span.getFont(); + if (font == null) { + RectBounds bounds = span.getBounds(); + run.setEmbedded(bounds, span.getText().length()); + return; + } + strike = font.getStrike(IDENTITY); + } else { + font = this.font; + strike = this.strike; + } + + /* init metrics for line breaks for empty lines */ + if (run.getAscent() == 0) { + Metrics m = strike.getMetrics(); + + /* The implementation of the center layoutBounds mode is to assure the + * layout has the same number of pixels above and bellow the cap + * height. + */ + if ((flags & BOUNDS_MASK) == BOUNDS_CENTER) { + float ascent = m.getAscent(); + /* Segoe UI has a very large internal leading area, applying the + * center layoutBounds heuristics on it would result in several pixels + * being added to the descent. The final results would be + * overly large and visually unappealing. The fix is to reduce + * the ascent before applying the algorithm. */ + if (font.getFamilyName().equals("Segoe UI")) { + ascent *= 0.80; + } + ascent = (int)(ascent-0.75); + float descent = (int)(m.getDescent()+0.75); + float leading = (int)(m.getLineGap()+0.75); + float capHeight = (int)(m.getCapHeight()+0.75); + float topPadding = -ascent - capHeight; + if (topPadding > descent) { + descent = topPadding; + } else { + ascent += (topPadding - descent); + } + run.setMetrics(ascent, descent, leading); + } else { + run.setMetrics(m.getAscent(), m.getDescent(), m.getLineGap()); + } + } + + if (run.isTab()) return; + if (run.isLinebreak()) return; + if (run.getGlyphCount() > 0) return; + if (run.isComplex()) { + /* Use GlyphLayout to shape complex text */ + layout.layout(run, font, strike, chars); + } else { + FontResource fr = strike.getFontResource(); + int start = run.getStart(); + int length = run.getLength(); + + /* No glyph layout required */ + if (layoutCache == null) { + float fontSize = strike.getSize(); + CharToGlyphMapper mapper = fr.getGlyphMapper(); + + /* The text contains complex and non-complex runs */ + int[] glyphs = new int[length]; + mapper.charsToGlyphs(start, length, chars, glyphs); + float[] positions = new float[(length + 1) << 1]; + float xadvance = 0; + for (int i = 0; i < length; i++) { + float width = fr.getAdvance(glyphs[i], fontSize); + positions[i<<1] = xadvance; + //yadvance always zero + xadvance += width; + } + positions[length<<1] = xadvance; + run.shape(length, glyphs, positions, null); + } else { + + /* The text only contains non-complex runs, all the glyphs and + * advances are stored in the shapeCache */ + if (!layoutCache.valid) { + float fontSize = strike.getSize(); + CharToGlyphMapper mapper = fr.getGlyphMapper(); + mapper.charsToGlyphs(start, length, chars, layoutCache.glyphs, start); + int end = start + length; + float width = 0; + for (int i = start; i < end; i++) { + float adv = fr.getAdvance(layoutCache.glyphs[i], fontSize); + layoutCache.advances[i] = adv; + width += adv; + } + run.setWidth(width); + } + run.shape(length, layoutCache.glyphs, layoutCache.advances); + } + } + } + + private TextLine createLine(int start, int end, int startOffset, float collapsedSpaceWidth) { + int count = end - start + 1; + + assert count > 0 : "number of TextRuns in a TextLine cannot be less than one: " + count; + + TextRun[] lineRuns = new TextRun[count]; + if (start < runCount) { + System.arraycopy(runs, start, lineRuns, 0, count); + } + + /* Recompute line width, height, and length (wrapping) */ + float width = 0, ascent = 0, descent = 0, leading = 0; + int length = 0; + for (int i = 0; i < lineRuns.length; i++) { + TextRun run = lineRuns[i]; + width += run.getWidth(); + ascent = Math.min(ascent, run.getAscent()); + descent = Math.max(descent, run.getDescent()); + leading = Math.max(leading, run.getLeading()); + length += run.getLength(); + } + + width -= collapsedSpaceWidth; + + if (width > layoutWidth) layoutWidth = width; + return new TextLine(startOffset, length, lineRuns, + width, ascent, descent, leading); + } + + /** + * Computes the size of the white space trailing a given run. + * + * @param run the run to compute trailing space width for, cannot be {@code null} + * @return the X size of the white space trailing the run + */ + private float computeTrailingSpaceWidth(TextRun run) { + float trailingSpaceWidth = 0; + char[] chars = getText(); + + /* + * As the loop below exits when encountering a non-white space character, + * testing each trailing glyph in turn for white space is safe, as white + * space is always represented with only a single glyph: + */ + + for (int i = run.getGlyphCount() - 1; i >= 0; i--) { + int textOffset = run.getStart() + run.getCharOffset(i); + + if (!Character.isWhitespace(chars[textOffset])) { + break; + } + + trailingSpaceWidth += run.getAdvance(i); + } + + return trailingSpaceWidth; + } + + private void reorderLine(TextLine line) { + TextRun[] runs = line.getRuns(); + int length = runs.length; + if (length > 0 && runs[length - 1].isLinebreak()) { + length--; + } + if (length < 2) return; + byte[] levels = new byte[length]; + for (int i = 0; i < length; i++) { + levels[i] = runs[i].getLevel(); + } + Bidi.reorderVisually(levels, 0, runs, 0, length); + } + + private char[] getText() { + if (text == null) { + int count = 0; + for (int i = 0; i < spans.length; i++) { + count += spans[i].getText().length(); + } + text = new char[count]; + int offset = 0; + for (int i = 0; i < spans.length; i++) { + String string = spans[i].getText(); + int length = string.length(); + string.getChars(0, length, text, offset); + offset += length; + } + } + return text; + } + + private boolean isSimpleLayout() { + int textAlignment = flags & ALIGN_MASK; + boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; + int mask = FLAGS_HAS_BIDI | FLAGS_HAS_COMPLEX; + return (flags & mask) == 0 && !justify; + } + + private boolean isMirrored() { + boolean mirrored = false; + switch (flags & DIRECTION_MASK) { + case DIRECTION_RTL: mirrored = true; break; + case DIRECTION_LTR: mirrored = false; break; + case DIRECTION_DEFAULT_LTR: + case DIRECTION_DEFAULT_RTL: + mirrored = (flags & FLAGS_RTL_BASE) != 0; + } + return mirrored; + } + + private float getMirroringWidth() { + /* The text node in the scene layer is mirrored based on + * result of computeLayoutBounds. The coordinate translation + * in text layout has to be based on the same width. + */ + return wrapWidth != 0 ? wrapWidth : layoutWidth; + } + + private void reuseRuns() { + /* The runs list is always accessed by the same thread (as TextLayout + * is not thread safe) thus it can be modified at any time, but the + * elements inside of the list are shared among threads and cannot be + * modified. Each reused element has to be cloned.*/ + runCount = 0; + int index = 0; + while (index < runs.length) { + TextRun run = runs[index]; + if (run == null) break; + runs[index] = null; + index++; + runs[runCount++] = run = run.unwrap(); + + if (run.isSplit()) { + run.merge(null); /* unmark split */ + while (index < runs.length) { + TextRun nextRun = runs[index]; + if (nextRun == null) break; + run.merge(nextRun); + runs[index] = null; + index++; + if (nextRun.isSplitLast()) break; + } + } + } + } + + private float getTabAdvance() { + float spaceAdvance = 0; + if (spans != null) { + /* Rich text case - use the first font (for now) */ + for (int i = 0; i < spans.length; i++) { + TextSpan span = spans[i]; + PGFont font = (PGFont)span.getFont(); + if (font != null) { + FontStrike strike = font.getStrike(IDENTITY); + spaceAdvance = strike.getCharAdvance(' '); + break; + } + } + } else { + spaceAdvance = strike.getCharAdvance(' '); + } + return tabSize * spaceAdvance; + } + + /* + * The way JavaFX lays out text: + * + * JavaFX distinguishes between soft wraps and hard wraps. Soft wraps + * occur when a wrap width has been set and the text requires wrapping + * to stay within the set wrap width. Hard wraps are explicitly part of + * the text in the form of line feeds (LF) and carriage returns (CR). + * Hard wrapping considers a singular LF or CR, or the combination of + * CR+LF (or LF+CR) as a single wrap location. Hard wrapping also occurs + * between TextSpans when multiple TextSpans were supplied (for wrapping + * purposes, there is no difference between two TextSpans and a single + * TextSpan where the text was concatenated with a line break in between). + * + * Soft wrapping occurs when a wrap width has been set. This occurs at + * the first character that does not fit. + * + * - If that character is not a white space, the break is set immediately + * after the first white space encountered before that character + * - If there is no white space before the preferred break character, the + * break is done at the first character that does not fit (the wrap + * then occurs in the middle of a (long) word) + * - If the preferred break character is white space, and it is followed by + * more white space, the break is moved to the end of the white space (thus + * a break in white space always occurs at first non white space character + * following a white space sequence) + * + * White space collapsing: + * + * Only white space that is present at soft wrapped locations is collapsed to + * zero. Any other white space is preserved. This includes white space between + * words, leading and trailing white space, and white space around hard wrapped + * locations. + * + * Alignment: + * + * The alignment calculation only looks at the width of all the significant + * characters in each line. Significant characters are any non white space + * characters and any white space that has been preserved (white space that wasn't + * collapsed due to soft wrapping). + * + * Alignment does not take text effects, such as strike through and underline, into + * account. This means that such effects can appear unaligned. Trailing spaces at a + * soft wrap location (that are underlined for example), may show the underline go + * outside the logical bounds of the text. + * + * Example, where indicates a soft wrap location, and is a line feed: + * + * " The quick brown fox jumps over the lazy dog " + * + * Would be rendered as (left aligned): + * + * " The quick" + * "brown fox jumps" + * "over the " + * " lazy dog " + * + * The alignment calculation uses the above bounds indicated by the double + * quotes, and so right aligned text would look like: + * + * " The quick" + * "brown fox jumps" + * "over the " + * " lazy dog " + * + * Note that only the white space at the soft wrap locations is collapsed. + * In all other locations the space was preserved (the space between words + * where no soft wrap occurred, the leading and trailing space, and the + * space around the hard wrapped location). + * + * Text effects have no effect on the alignment, and so with underlining on + * the right aligned text would look like: + * + * "___The___quick_" (one collapsed space becomes visible here) + * "brown_fox_jumps__" (two collapsed spaces become visible here) + * "over_the_" + * "_lazy_dog___" + * + * Note that text alignment has not changed at all, but the bounds are exceeded + * in some locations to allow for the underline. Controls displaying such texts + * will likely clip the underlined parts exceeding the bounds. + * + * Users wishing to mitigate some of these perhaps surprising results can ensure + * they use trimmed texts, and avoid the use of line breaks, or at least ensure + * that line breaks are not preceded or succeeded by white space (activating + * line wrapping is not equivalent to collapsing any consecutive white space + * no matter where it occurs). + */ + + private void layout() { + /* Try the cache */ + initCache(); + + /* Whole layout retrieved from the cache */ + if (lines != null) return; + char[] chars = getText(); + + /* runs and runCount are set in reuseRuns or buildRuns */ + if ((flags & FLAGS_ANALYSIS_VALID) != 0 && isSimpleLayout()) { + reuseRuns(); + } else { + buildRuns(chars); + } + + GlyphLayout layout = null; + if ((flags & (FLAGS_HAS_COMPLEX)) != 0) { + layout = glyphLayout(); + } + + float tabAdvance = 0; + if ((flags & FLAGS_HAS_TABS) != 0) { + tabAdvance = getTabAdvance(); + } + + BreakIterator boundary = null; + if (wrapWidth > 0) { + if ((flags & (FLAGS_HAS_COMPLEX | FLAGS_HAS_CJK)) != 0) { + boundary = BreakIterator.getLineInstance(); + boundary.setText(new CharArrayIterator(chars)); + } + } + int textAlignment = flags & ALIGN_MASK; + + /* Optimize simple case: reuse the glyphs and advances as long as the + * text and font are the same. + * The simple case is no bidi, no complex, no justify, no features. + */ + + if (isSimpleLayout()) { + if (layoutCache == null) { + layoutCache = new LayoutCache(); + layoutCache.glyphs = new int[chars.length]; + layoutCache.advances = new float[chars.length]; + } + } else { + layoutCache = null; + } + + float lineWidth = 0; + int startIndex = 0; + int startOffset = 0; + ArrayList linesList = new ArrayList<>(); + for (int i = 0; i < runCount; i++) { + TextRun run = runs[i]; + shape(run, chars, layout); + if (run.isTab()) { + float tabStop = ((int)(lineWidth / tabAdvance) +1) * tabAdvance; + run.setWidth(tabStop - lineWidth); + } + + float runWidth = run.getWidth(); + if (wrapWidth > 0 && lineWidth + runWidth > wrapWidth && !run.isLinebreak()) { + + /* Find offset of the first character that does not fit on the line */ + int hitOffset = run.getStart() + run.getWrapIndex(wrapWidth - lineWidth); + + /* + * Only keep white spaces (not tabs) in the current run to avoid + * dealing with unshaped runs. + * + * If the run is a tab, the run will be always of length 1 (see + * buildRuns()). As there is no "next" character that can be selected + * as the wrap index in this run, the white space skipping logic + * below won't skip tabs. + */ + + int offset = hitOffset; + int runEnd = run.getEnd(); + + // Don't take white space into account at the preferred wrap index: + while (offset + 1 < runEnd && Character.isWhitespace(chars[offset])) { + offset++; + } + + /* Find the break opportunity */ + int breakOffset = offset; + if (boundary != null) { + /* Use Java BreakIterator when complex script are present */ + breakOffset = boundary.isBoundary(offset) || chars[offset] == '\t' ? offset : boundary.preceding(offset); + } else { + /* Simple break strategy for latin text (Performance) */ + boolean currentChar = Character.isWhitespace(chars[breakOffset]); + while (breakOffset > startOffset) { + boolean previousChar = Character.isWhitespace(chars[breakOffset - 1]); + if (!currentChar && previousChar) break; + currentChar = previousChar; + breakOffset--; + } + } + + /* Never break before the line start offset */ + if (breakOffset < startOffset) breakOffset = startOffset; + + /* Find the run that contains the break offset */ + int breakRunIndex = startIndex; + TextRun breakRun = null; + while (breakRunIndex < runCount) { + breakRun = runs[breakRunIndex]; + if (breakRun.getEnd() > breakOffset) break; + breakRunIndex++; + } + + /* No line breaks between hit offset and line start offset. + * Try character wrapping mode at the hit offset. + */ + if (breakOffset == startOffset) { + breakRun = run; + breakRunIndex = i; + breakOffset = hitOffset; + } + + int breakOffsetInRun = breakOffset - breakRun.getStart(); + /* Wrap the entire run to the next (only if it is not the first + * run of the line). + */ + if (breakOffsetInRun == 0 && breakRunIndex != startIndex) { + i = breakRunIndex - 1; + } else { + i = breakRunIndex; + + /* The break offset is at the first offset of the first run of the line. + * This happens when the wrap width is smaller than the width require + * to show the first character for the line. + */ + if (breakOffsetInRun == 0) { + breakOffsetInRun++; + } + if (breakOffsetInRun < breakRun.getLength()) { + if (runCount >= runs.length) { + TextRun[] newRuns = new TextRun[runs.length + 64]; + System.arraycopy(runs, 0, newRuns, 0, i + 1); + System.arraycopy(runs, i + 1, newRuns, i + 2, runs.length - i - 1); + runs = newRuns; + } else { + System.arraycopy(runs, i + 1, runs, i + 2, runCount - i - 1); + } + runs[i + 1] = breakRun.split(breakOffsetInRun); + if (breakRun.isComplex()) { + shape(breakRun, chars, layout); + } + runCount++; + } + } + + /* No point marking the last run of a line a softbreak */ + if (i + 1 < runCount && !runs[i + 1].isLinebreak()) { + run = runs[i]; + run.setSoftbreak(); + flags |= FLAGS_WRAPPED; + + // Tabs should preserve width + + /* + * Due to contextual forms (arabic) it is possible this line + * is still too big since the splitting of the arabic run + * changes the shape of boundary glyphs. For now the + * implementation has opted to have the appropriate + * initial/final shapes and allow those glyphs to + * potentially overlap the wrapping width, rather than use + * the medial form within the wrappingWidth. A better place + * to solve this would be TextRun#getWrapIndex - but its TBD + * there too. + */ + } + } + + lineWidth += runWidth; + if (run.isBreak()) { + TextLine line = createLine(startIndex, i, startOffset, computeTrailingSpaceWidth(runs[i])); + linesList.add(line); + startIndex = i + 1; + startOffset += line.getLength(); + lineWidth = 0; + } + } + if (layout != null) layout.dispose(); + + linesList.add(createLine(startIndex, runCount - 1, startOffset, 0)); + lines = new TextLine[linesList.size()]; + linesList.toArray(lines); + + float fullWidth = wrapWidth > 0 ? wrapWidth : layoutWidth; // layoutWidth = widest line, wrapWidth is user set + float lineY = 0; + float align; + if (isMirrored()) { + align = 1; /* Left and Justify */ + if (textAlignment == ALIGN_RIGHT) align = 0; + } else { + align = 0; /* Left and Justify */ + if (textAlignment == ALIGN_RIGHT) align = 1; + } + if (textAlignment == ALIGN_CENTER) align = 0.5f; + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + int lineStart = line.getStart(); + RectBounds bounds = line.getBounds(); + + /* Center and right alignment */ + float unusedWidth = fullWidth - bounds.getWidth(); + float lineX = unusedWidth * align; + line.setAlignment(lineX); + + /* Justify */ + boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; + if (justify) { + TextRun[] lineRuns = line.getRuns(); + int lineRunCount = lineRuns.length; + if (lineRunCount > 0 && lineRuns[lineRunCount - 1].isSoftbreak()) { + /* count white spaces but skipping trailings whitespaces */ + int lineEnd = lineStart + line.getLength(); + int wsCount = 0; + boolean hitChar = false; + for (int j = lineEnd - 1; j >= lineStart; j--) { + if (!hitChar && chars[j] != ' ') hitChar = true; + if (hitChar && chars[j] == ' ') wsCount++; + } + if (wsCount != 0) { + float inc = unusedWidth / wsCount; + done: + for (int j = 0; j < lineRunCount; j++) { + TextRun textRun = lineRuns[j]; + int runStart = textRun.getStart(); + int runEnd = textRun.getEnd(); + for (int k = runStart; k < runEnd; k++) { + // TODO kashidas + if (chars[k] == ' ') { + textRun.justify(k - runStart, inc); + if (--wsCount == 0) break done; + } + } + } + lineX = 0; + line.setAlignment(lineX); + line.setWidth(fullWidth); + } + } + } + + if ((flags & FLAGS_HAS_BIDI) != 0) { + reorderLine(line); + } + + computeSideBearings(line); + + /* Set run location */ + float runX = lineX; + TextRun[] lineRuns = line.getRuns(); + for (int j = 0; j < lineRuns.length; j++) { + TextRun run = lineRuns[j]; + run.setLocation(runX, lineY); + run.setLine(line); + runX += run.getWidth(); + } + if (i + 1 < lines.length) { + lineY = Math.max(lineY, lineY + bounds.getHeight() + spacing); + } else { + lineY += (bounds.getHeight() - line.getLeading()); + } + } + float ascent = lines[0].getBounds().getMinY(); + layoutHeight = lineY; + logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, layoutWidth, + layoutHeight + ascent, 0); + + + if (layoutCache != null) { + if (cacheKey != null && !layoutCache.valid && !copyCache()) { + /* After layoutCache is added to the stringCache it can be + * accessed by multiple threads. All the data in it must + * be immutable. See copyCache() for the cases where the entire + * layout is immutable. + */ + layoutCache.font = font; + layoutCache.text = text; + layoutCache.runs = runs; + layoutCache.runCount = runCount; + layoutCache.lines = lines; + layoutCache.layoutWidth = layoutWidth; + layoutCache.layoutHeight = layoutHeight; + layoutCache.analysis = flags & ANALYSIS_MASK; + synchronized (CACHE_SIZE_LOCK) { + int charCount = chars.length; + if (cacheSize + charCount > MAX_CACHE_SIZE) { + stringCache.clear(); + cacheSize = 0; + } + stringCache.put(cacheKey, layoutCache); + cacheSize += charCount; + } + } + layoutCache.valid = true; + } + } + + @Override + public BaseBounds getVisualBounds(int type) { + ensureLayout(); + + /* Not defined for rich text */ + if (strike == null) { + return null; + } + + boolean underline = (type & TYPE_UNDERLINE) != 0; + boolean hasUnderline = (flags & FLAGS_CACHED_UNDERLINE) != 0; + boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; + boolean hasStrikethrough = (flags & FLAGS_CACHED_STRIKETHROUGH) != 0; + if (visualBounds != null && underline == hasUnderline + && strikethrough == hasStrikethrough) { + /* Return last cached value */ + return visualBounds; + } + + flags &= ~(FLAGS_CACHED_STRIKETHROUGH | FLAGS_CACHED_UNDERLINE); + if (underline) flags |= FLAGS_CACHED_UNDERLINE; + if (strikethrough) flags |= FLAGS_CACHED_STRIKETHROUGH; + visualBounds = new RectBounds(); + + float xMin = Float.POSITIVE_INFINITY; + float yMin = Float.POSITIVE_INFINITY; + float xMax = Float.NEGATIVE_INFINITY; + float yMax = Float.NEGATIVE_INFINITY; + float bounds[] = new float[4]; + FontResource fr = strike.getFontResource(); + Metrics metrics = strike.getMetrics(); + float size = strike.getSize(); + for (int i = 0; i < lines.length; i++) { + TextLine line = lines[i]; + TextRun[] runs = line.getRuns(); + for (int j = 0; j < runs.length; j++) { + TextRun run = runs[j]; + Point2D pt = run.getLocation(); + if (run.isLinebreak()) continue; + int glyphCount = run.getGlyphCount(); + for (int gi = 0; gi < glyphCount; gi++) { + int gc = run.getGlyphCode(gi); + if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { + fr.getGlyphBoundingBox(run.getGlyphCode(gi), size, bounds); + if (bounds[X_MIN_INDEX] != bounds[X_MAX_INDEX]) { + float glyphX = pt.x + run.getPosX(gi); + float glyphY = pt.y + run.getPosY(gi); + float glyphMinX = glyphX + bounds[X_MIN_INDEX]; + float glyphMinY = glyphY - bounds[Y_MAX_INDEX]; + float glyphMaxX = glyphX + bounds[X_MAX_INDEX]; + float glyphMaxY = glyphY - bounds[Y_MIN_INDEX]; + if (glyphMinX < xMin) xMin = glyphMinX; + if (glyphMinY < yMin) yMin = glyphMinY; + if (glyphMaxX > xMax) xMax = glyphMaxX; + if (glyphMaxY > yMax) yMax = glyphMaxY; + } + } + } + if (underline) { + float underlineMinX = pt.x; + float underlineMinY = pt.y + metrics.getUnderLineOffset(); + float underlineMaxX = underlineMinX + run.getWidth(); + float underlineMaxY = underlineMinY + metrics.getUnderLineThickness(); + if (underlineMinX < xMin) xMin = underlineMinX; + if (underlineMinY < yMin) yMin = underlineMinY; + if (underlineMaxX > xMax) xMax = underlineMaxX; + if (underlineMaxY > yMax) yMax = underlineMaxY; + } + if (strikethrough) { + float strikethroughMinX = pt.x; + float strikethroughMinY = pt.y + metrics.getStrikethroughOffset(); + float strikethroughMaxX = strikethroughMinX + run.getWidth(); + float strikethroughMaxY = strikethroughMinY + metrics.getStrikethroughThickness(); + if (strikethroughMinX < xMin) xMin = strikethroughMinX; + if (strikethroughMinY < yMin) yMin = strikethroughMinY; + if (strikethroughMaxX > xMax) xMax = strikethroughMaxX; + if (strikethroughMaxY > yMax) yMax = strikethroughMaxY; + } + } + } + + if (xMin < xMax && yMin < yMax) { + visualBounds.setBounds(xMin, yMin, xMax, yMax); + } + return visualBounds; + } + + private void computeSideBearings(TextLine line) { + TextRun[] runs = line.getRuns(); + if (runs.length == 0) return; + float bounds[] = new float[4]; + FontResource defaultFontResource = null; + float size = 0; + if (strike != null) { + defaultFontResource = strike.getFontResource(); + size = strike.getSize(); + } + + /* The line lsb is the lsb of the first visual character in the line */ + float lsb = 0; + float width = 0; + lsbdone: + for (int i = 0; i < runs.length; i++) { + TextRun run = runs[i]; + int glyphCount = run.getGlyphCount(); + for (int gi = 0; gi < glyphCount; gi++) { + float advance = run.getAdvance(gi); + /* Skip any leading zero-width glyphs in the line */ + if (advance != 0) { + int gc = run.getGlyphCode(gi); + /* Skip any leading invisible glyphs in the line */ + if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { + FontResource fr = defaultFontResource; + if (fr == null) { + TextSpan span = run.getTextSpan(); + PGFont font = (PGFont)span.getFont(); + /* No need to check font != null (run.glyphCount > 0) */ + size = font.getSize(); + fr = font.getFontResource(); + } + fr.getGlyphBoundingBox(gc, size, bounds); + float glyphLsb = bounds[X_MIN_INDEX]; + lsb = Math.min(0, glyphLsb + width); + run.setLeftBearing(); + break lsbdone; + } + } + width += advance; + } + // tabs + if (glyphCount == 0) { + width += run.getWidth(); + } + } + + /* The line rsb is the rsb of the last visual character in the line */ + float rsb = 0; + width = 0; + rsbdone: + for (int i = runs.length - 1; i >= 0 ; i--) { + TextRun run = runs[i]; + int glyphCount = run.getGlyphCount(); + for (int gi = glyphCount - 1; gi >= 0; gi--) { + float advance = run.getAdvance(gi); + /* Skip any trailing zero-width glyphs in the line */ + if (advance != 0) { + int gc = run.getGlyphCode(gi); + /* Skip any trailing invisible glyphs in the line */ + if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { + FontResource fr = defaultFontResource; + if (fr == null) { + TextSpan span = run.getTextSpan(); + PGFont font = (PGFont)span.getFont(); + /* No need to check font != null (run.glyphCount > 0) */ + size = font.getSize(); + fr = font.getFontResource(); + } + fr.getGlyphBoundingBox(gc, size, bounds); + float glyphRsb = bounds[X_MAX_INDEX] - advance; + rsb = Math.max(0, glyphRsb - width); + run.setRightBearing(); + break rsbdone; + } + } + width += advance; + } + // tabs + if (glyphCount == 0) { + width += run.getWidth(); + } + } + line.setSideBearings(lsb, rsb); } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutBase.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutBase.java deleted file mode 100644 index 581e59e8689..00000000000 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutBase.java +++ /dev/null @@ -1,1674 +0,0 @@ -/* - * Copyright (c) 2012, 2025, 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 com.sun.javafx.text; - -import java.text.Bidi; -import java.text.BreakIterator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Hashtable; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; -import javafx.scene.shape.LineTo; -import javafx.scene.shape.MoveTo; -import javafx.scene.shape.PathElement; -import com.sun.javafx.font.CharToGlyphMapper; -import com.sun.javafx.font.FontResource; -import com.sun.javafx.font.FontStrike; -import com.sun.javafx.font.Metrics; -import com.sun.javafx.font.PGFont; -import com.sun.javafx.geom.BaseBounds; -import com.sun.javafx.geom.Path2D; -import com.sun.javafx.geom.Point2D; -import com.sun.javafx.geom.RectBounds; -import com.sun.javafx.geom.RoundRectangle2D; -import com.sun.javafx.geom.Shape; -import com.sun.javafx.geom.transform.BaseTransform; -import com.sun.javafx.geom.transform.Translate2D; -import com.sun.javafx.scene.text.GlyphList; -import com.sun.javafx.scene.text.TextLayout; -import com.sun.javafx.scene.text.TextSpan; - -/** - * Original name: PrismTextLayout - */ -public class PrismTextLayoutBase implements TextLayout { - private static final BaseTransform IDENTITY = BaseTransform.IDENTITY_TRANSFORM; - private static final int X_MIN_INDEX = 0; - private static final int Y_MIN_INDEX = 1; - private static final int X_MAX_INDEX = 2; - private static final int Y_MAX_INDEX = 3; - - private static final Hashtable stringCache = new Hashtable<>(); - private static final Object CACHE_SIZE_LOCK = new Object(); - private static int cacheSize = 0; - private static final int MAX_STRING_SIZE = 256; - private final int MAX_CACHE_SIZE; - private final Supplier layoutSupplier; - - private char[] text; - private TextSpan[] spans; /* Rich text (null for single font text) */ - private PGFont font; /* Single font text (null for rich text) */ - private FontStrike strike; /* cached strike of font (identity) */ - private Integer cacheKey; - private TextLine[] lines; - private TextRun[] runs; - private int runCount; - private BaseBounds logicalBounds; - private RectBounds visualBounds; - private float layoutWidth, layoutHeight; - private float wrapWidth, spacing; - private LayoutCache layoutCache; - private Shape shape; - private int flags; - private int tabSize = DEFAULT_TAB_SIZE; - - public PrismTextLayoutBase(int maxCacheSize, Supplier ls) { - MAX_CACHE_SIZE = maxCacheSize; - layoutSupplier = ls; - logicalBounds = new RectBounds(); - flags = ALIGN_LEFT; - } - - private void reset() { - layoutCache = null; - runs = null; - flags &= ~ANALYSIS_MASK; - relayout(); - } - - private void relayout() { - logicalBounds.makeEmpty(); - visualBounds = null; - layoutWidth = layoutHeight = 0; - flags &= ~(FLAGS_WRAPPED | FLAGS_CACHED_UNDERLINE | FLAGS_CACHED_STRIKETHROUGH); - lines = null; - shape = null; - } - - /*************************************************************************** - * * - * TextLayout API * - * * - **************************************************************************/ - - @Override - public boolean setContent(TextSpan[] spans) { - if (spans == null && this.spans == null) return false; - if (spans != null && this.spans != null) { - if (spans.length == this.spans.length) { - int i = 0; - while (i < spans.length) { - if (spans[i] != this.spans[i]) break; - i++; - } - if (i == spans.length) return false; - } - } - - reset(); - this.spans = spans; - this.font = null; - this.strike = null; - this.text = null; /* Initialized in getText() */ - this.cacheKey = null; - return true; - } - - @Override - public boolean setContent(String text, Object font) { - reset(); - this.spans = null; - this.font = (PGFont)font; - this.strike = ((PGFont)font).getStrike(IDENTITY); - this.text = text.toCharArray(); - if (MAX_CACHE_SIZE > 0) { - int length = text.length(); - if (0 < length && length <= MAX_STRING_SIZE) { - cacheKey = text.hashCode() * strike.hashCode(); - } - } - return true; - } - - @Override - public boolean setDirection(int direction) { - if ((flags & DIRECTION_MASK) == direction) return false; - flags &= ~DIRECTION_MASK; - flags |= (direction & DIRECTION_MASK); - reset(); - return true; - } - - @Override - public boolean setBoundsType(int type) { - if ((flags & BOUNDS_MASK) == type) return false; - flags &= ~BOUNDS_MASK; - flags |= (type & BOUNDS_MASK); - reset(); - return true; - } - - @Override - public boolean setAlignment(int alignment) { - int align = ALIGN_LEFT; - switch (alignment) { - case 0: align = ALIGN_LEFT; break; - case 1: align = ALIGN_CENTER; break; - case 2: align = ALIGN_RIGHT; break; - case 3: align = ALIGN_JUSTIFY; break; - } - if ((flags & ALIGN_MASK) == align) return false; - if (align == ALIGN_JUSTIFY || (flags & ALIGN_JUSTIFY) != 0) { - reset(); - } - flags &= ~ALIGN_MASK; - flags |= align; - relayout(); - return true; - } - - @Override - public boolean setWrapWidth(float newWidth) { - if (Float.isInfinite(newWidth)) newWidth = 0; - if (Float.isNaN(newWidth)) newWidth = 0; - float oldWidth = this.wrapWidth; - this.wrapWidth = Math.max(0, newWidth); - - boolean needsLayout = true; - if (lines != null && oldWidth != 0 && newWidth != 0) { - if ((flags & ALIGN_LEFT) != 0) { - if (newWidth > oldWidth) { - /* If wrapping width is increasing and there is no - * wrapped lines then the text remains valid. - */ - if ((flags & FLAGS_WRAPPED) == 0) { - needsLayout = false; - } - } else { - /* If wrapping width is decreasing but it is still - * greater than the max line width then the text - * remains valid. - */ - if (newWidth >= layoutWidth) { - needsLayout = false; - } - } - } - } - if (needsLayout) relayout(); - return needsLayout; - } - - @Override - public boolean setLineSpacing(float spacing) { - if (this.spacing == spacing) return false; - this.spacing = spacing; - relayout(); - return true; - } - - private void ensureLayout() { - if (lines == null) { - layout(); - } - } - - @Override - public com.sun.javafx.scene.text.TextLine[] getLines() { - ensureLayout(); - return lines; - } - - @Override - public GlyphList[] getRuns() { - ensureLayout(); - GlyphList[] result = new GlyphList[runCount]; - int count = 0; - for (int i = 0; i < lines.length; i++) { - GlyphList[] lineRuns = lines[i].getRuns(); - int length = lineRuns.length; - System.arraycopy(lineRuns, 0, result, count, length); - count += length; - } - return result; - } - - @Override - public BaseBounds getBounds() { - ensureLayout(); - return logicalBounds; - } - - @Override - public BaseBounds getBounds(TextSpan filter, BaseBounds bounds) { - ensureLayout(); - float left = Float.POSITIVE_INFINITY; - float top = Float.POSITIVE_INFINITY; - float right = Float.NEGATIVE_INFINITY; - float bottom = Float.NEGATIVE_INFINITY; - if (filter != null) { - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - TextRun[] lineRuns = line.getRuns(); - for (int j = 0; j < lineRuns.length; j++) { - TextRun run = lineRuns[j]; - TextSpan span = run.getTextSpan(); - if (span != filter) continue; - Point2D location = run.getLocation(); - float runLeft = location.x; - if (run.isLeftBearing()) { - runLeft += line.getLeftSideBearing(); - } - float runRight = location.x + run.getWidth(); - if (run.isRightBearing()) { - runRight += line.getRightSideBearing(); - } - float runTop = location.y; - float runBottom = location.y + line.getBounds().getHeight() + spacing; - if (runLeft < left) left = runLeft; - if (runTop < top) top = runTop; - if (runRight > right) right = runRight; - if (runBottom > bottom) bottom = runBottom; - } - } - } else { - top = bottom = 0; - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - RectBounds lineBounds = line.getBounds(); - float lineLeft = lineBounds.getMinX() + line.getLeftSideBearing(); - if (lineLeft < left) left = lineLeft; - float lineRight = lineBounds.getMaxX() + line.getRightSideBearing(); - if (lineRight > right) right = lineRight; - bottom += lineBounds.getHeight(); - } - if (isMirrored()) { - float width = getMirroringWidth(); - float bearing = left; - left = width - right; - right = width - bearing; - } - } - return bounds.deriveWithNewBounds(left, top, 0, right, bottom, 0); - } - - @Override - public PathElement[] getCaretShape(int offset, boolean isLeading, - float x, float y) { - ensureLayout(); - int lineIndex = 0; - int lineCount = getLineCount(); - while (lineIndex < lineCount - 1) { - TextLine line = lines[lineIndex]; - int lineEnd = line.getStart() + line.getLength(); - if (lineEnd > offset) break; - lineIndex++; - } - int splitCaretOffset = -1; - int level = 0; - float lineX = 0, lineY = 0, lineHeight = 0; - TextLine line = lines[lineIndex]; - TextRun[] runs = line.getRuns(); - int runCount = runs.length; - int runIndex = -1; - for (int i = 0; i < runCount; i++) { - TextRun run = runs[i]; - int runStart = run.getStart(); - int runEnd = run.getEnd(); - if (runStart <= offset && offset < runEnd) { - if (!run.isLinebreak()) { - runIndex = i; - } - break; - } - } - if (runIndex != -1) { - TextRun run = runs[runIndex]; - int runStart = run.getStart(); - Point2D location = run.getLocation(); - lineX = location.x + run.getXAtOffset(offset - runStart, isLeading); - lineY = location.y; - lineHeight = line.getBounds().getHeight(); - - if (isLeading) { - if (runIndex > 0 && offset == runStart) { - level = run.getLevel(); - splitCaretOffset = offset - 1; - } - } else { - int runEnd = run.getEnd(); - if (runIndex + 1 < runs.length && offset + 1 == runEnd) { - level = run.getLevel(); - splitCaretOffset = offset + 1; - } - } - } else { - /* end of line (line break or offset>=charCount) */ - int maxOffset = 0; - - /* set run index to zero to handle empty line case (only break line) */ - runIndex = 0; - for (int i = 0; i < runCount; i++) { - TextRun run = runs[i]; - /*use the trailing edge of the last logical run*/ - if (run.getStart() >= maxOffset && !run.isLinebreak()) { - maxOffset = run.getStart(); - runIndex = i; - } - } - TextRun run = runs[runIndex]; - Point2D location = run.getLocation(); - lineX = location.x + (run.isLeftToRight() ? run.getWidth() : 0); - lineY = location.y; - lineHeight = line.getBounds().getHeight(); - } - if (isMirrored()) { - lineX = getMirroringWidth() - lineX; - } - lineX += x; - lineY += y; - if (splitCaretOffset != -1) { - for (int i = 0; i < runs.length; i++) { - TextRun run = runs[i]; - int runStart = run.getStart(); - int runEnd = run.getEnd(); - if (runStart <= splitCaretOffset && splitCaretOffset < runEnd) { - if ((run.getLevel() & 1) != (level & 1)) { - Point2D location = run.getLocation(); - float lineX2 = location.x; - if (isLeading) { - if ((level & 1) != 0) lineX2 += run.getWidth(); - } else { - if ((level & 1) == 0) lineX2 += run.getWidth(); - } - if (isMirrored()) { - lineX2 = getMirroringWidth() - lineX2; - } - lineX2 += x; - PathElement[] result = new PathElement[4]; - result[0] = new MoveTo(lineX, lineY); - result[1] = new LineTo(lineX, lineY + lineHeight / 2); - result[2] = new MoveTo(lineX2, lineY + lineHeight / 2); - result[3] = new LineTo(lineX2, lineY + lineHeight); - return result; - } - } - } - } - PathElement[] result = new PathElement[2]; - result[0] = new MoveTo(lineX, lineY); - result[1] = new LineTo(lineX, lineY + lineHeight); - return result; - } - - @Override - public Hit getHitInfo(float x, float y) { - int charIndex = -1; - int insertionIndex = -1; - boolean leading = false; - - ensureLayout(); - int lineIndex = getLineIndex(y); - if (lineIndex >= getLineCount()) { - charIndex = getCharCount(); - insertionIndex = charIndex + 1; - } else { - TextLine line = lines[lineIndex]; - TextRun[] runs = line.getRuns(); - RectBounds bounds = line.getBounds(); - TextRun run = null; - x -= bounds.getMinX(); - 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(); - } - } - if (run != null) { - AtomicBoolean trailing = new AtomicBoolean(); - charIndex = run.getStart() + run.getOffsetAtX(x, trailing); - leading = !trailing.get(); - - insertionIndex = charIndex; - if (getText() != null && insertionIndex < getText().length) { - if (!leading) { - BreakIterator charIterator = BreakIterator.getCharacterInstance(); - charIterator.setText(new String(getText())); - int next = charIterator.following(insertionIndex); - if (next == BreakIterator.DONE) { - insertionIndex += 1; - } else { - insertionIndex = next; - } - } - } else if (!leading) { - insertionIndex += 1; - } - } else { - //empty line, set to line break leading - charIndex = line.getStart(); - leading = true; - insertionIndex = charIndex; - } - } - return new Hit(charIndex, insertionIndex, leading); - } - - @Override - public PathElement[] getRange(int start, int end, int type, - float x, float y) { - ensureLayout(); - int lineCount = getLineCount(); - ArrayList result = new ArrayList<>(); - float lineY = 0; - - for (int lineIndex = 0; lineIndex < lineCount; lineIndex++) { - TextLine line = lines[lineIndex]; - RectBounds lineBounds = line.getBounds(); - int lineStart = line.getStart(); - if (lineStart >= end) break; - int lineEnd = lineStart + line.getLength(); - if (start > lineEnd) { - lineY += lineBounds.getHeight() + spacing; - continue; - } - - /* The list of runs in the line is visually ordered. - * Thus, finding the run that includes the selection end offset - * does not mean that all selected runs have being visited. - * Instead, this implementation first computes the number of selected - * characters in the current line, then iterates over the runs consuming - * selected characters till all of them are found. - */ - TextRun[] runs = line.getRuns(); - int count = Math.min(lineEnd, end) - Math.max(lineStart, start); - int runIndex = 0; - float left = -1; - float right = -1; - float lineX = lineBounds.getMinX(); - while (count > 0 && runIndex < runs.length) { - TextRun run = runs[runIndex]; - int runStart = run.getStart(); - int runEnd = run.getEnd(); - float runWidth = run.getWidth(); - int clmapStart = Math.max(runStart, Math.min(start, runEnd)); - int clampEnd = Math.max(runStart, Math.min(end, runEnd)); - int runCount = clampEnd - clmapStart; - if (runCount != 0) { - boolean ltr = run.isLeftToRight(); - float runLeft; - if (runStart > start) { - runLeft = ltr ? lineX : lineX + runWidth; - } else { - runLeft = lineX + run.getXAtOffset(start - runStart, true); - } - float runRight; - if (runEnd < end) { - runRight = ltr ? lineX + runWidth : lineX; - } else { - runRight = lineX + run.getXAtOffset(end - runStart, true); - } - if (runLeft > runRight) { - float tmp = runLeft; - runLeft = runRight; - runRight = tmp; - } - count -= runCount; - float top = 0, bottom = 0; - switch (type) { - case TYPE_TEXT: - top = lineY; - bottom = lineY + lineBounds.getHeight(); - break; - case TYPE_UNDERLINE: - case TYPE_STRIKETHROUGH: - FontStrike fontStrike = null; - if (spans != null) { - TextSpan span = run.getTextSpan(); - PGFont font = (PGFont)span.getFont(); - if (font == null) break; - fontStrike = font.getStrike(IDENTITY); - } else { - fontStrike = strike; - } - top = lineY - run.getAscent(); - Metrics metrics = fontStrike.getMetrics(); - if (type == TYPE_UNDERLINE) { - top += metrics.getUnderLineOffset(); - bottom = top + metrics.getUnderLineThickness(); - } else { - top += metrics.getStrikethroughOffset(); - bottom = top + metrics.getStrikethroughThickness(); - } - break; - } - - /* Merge continuous rectangles */ - if (runLeft != right) { - if (left != -1 && right != -1) { - float l = left, r = right; - if (isMirrored()) { - float width = getMirroringWidth(); - l = width - l; - r = width - r; - } - result.add(new MoveTo(x + l, y + top)); - result.add(new LineTo(x + r, y + top)); - result.add(new LineTo(x + r, y + bottom)); - result.add(new LineTo(x + l, y + bottom)); - result.add(new LineTo(x + l, y + top)); - } - left = runLeft; - right = runRight; - } - right = runRight; - if (count == 0) { - float l = left, r = right; - if (isMirrored()) { - float width = getMirroringWidth(); - l = width - l; - r = width - r; - } - result.add(new MoveTo(x + l, y + top)); - result.add(new LineTo(x + r, y + top)); - result.add(new LineTo(x + r, y + bottom)); - result.add(new LineTo(x + l, y + bottom)); - result.add(new LineTo(x + l, y + top)); - } - } - lineX += runWidth; - runIndex++; - } - lineY += lineBounds.getHeight() + spacing; - } - return result.toArray(new PathElement[result.size()]); - } - - @Override - public Shape getShape(int type, TextSpan filter) { - ensureLayout(); - boolean text = (type & TYPE_TEXT) != 0; - boolean underline = (type & TYPE_UNDERLINE) != 0; - boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; - boolean baselineType = (type & TYPE_BASELINE) != 0; - if (shape != null && text && !underline && !strikethrough && baselineType) { - return shape; - } - - Path2D outline = new Path2D(); - BaseTransform tx = new Translate2D(0, 0); - /* Return a shape relative to the baseline of the first line so - * it can be used for layout */ - float firstBaseline = 0; - if (baselineType) { - firstBaseline = -lines[0].getBounds().getMinY(); - } - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - TextRun[] runs = line.getRuns(); - RectBounds bounds = line.getBounds(); - float baseline = -bounds.getMinY(); - for (int j = 0; j < runs.length; j++) { - TextRun run = runs[j]; - FontStrike fontStrike = null; - if (spans != null) { - TextSpan span = run.getTextSpan(); - if (filter != null && span != filter) continue; - PGFont font = (PGFont)span.getFont(); - - /* skip embedded runs */ - if (font == null) continue; - fontStrike = font.getStrike(IDENTITY); - } else { - fontStrike = strike; - } - Point2D location = run.getLocation(); - float runX = location.x; - float runY = location.y + baseline - firstBaseline; - Metrics metrics = null; - if (underline || strikethrough) { - metrics = fontStrike.getMetrics(); - } - if (underline) { - RoundRectangle2D rect = new RoundRectangle2D(); - rect.x = runX; - rect.y = runY + metrics.getUnderLineOffset(); - rect.width = run.getWidth(); - rect.height = metrics.getUnderLineThickness(); - outline.append(rect, false); - } - if (strikethrough) { - RoundRectangle2D rect = new RoundRectangle2D(); - rect.x = runX; - rect.y = runY + metrics.getStrikethroughOffset(); - rect.width = run.getWidth(); - rect.height = metrics.getStrikethroughThickness(); - outline.append(rect, false); - } - if (text && run.getGlyphCount() > 0) { - tx.restoreTransform(1, 0, 0, 1, runX, runY); - Path2D path = (Path2D)fontStrike.getOutline(run, tx); - outline.append(path, false); - } - } - } - - if (text && !underline && !strikethrough) { - shape = outline; - } - return outline; - } - - @Override - public boolean setTabSize(int spaces) { - if (spaces < 1) { - spaces = 1; - } - if (tabSize != spaces) { - tabSize = spaces; - relayout(); - return true; - } - return false; - } - - /*************************************************************************** - * * - * Text Layout Implementation * - * * - **************************************************************************/ - - private int getLineIndex(float y) { - int index = 0; - float bottom = 0; - - int lineCount = getLineCount(); - while (index < lineCount) { - bottom += lines[index].getBounds().getHeight() + spacing; - if (index + 1 == lineCount) { - bottom -= lines[index].getLeading(); - } - if (bottom > y) { - break; - } - index++; - } - return index; - } - - private boolean copyCache() { - int align = flags & ALIGN_MASK; - int boundsType = flags & BOUNDS_MASK; - /* Caching for boundsType == Center, bias towards Modena */ - return wrapWidth != 0 || align != ALIGN_LEFT || boundsType == 0 || isMirrored(); - } - - private void initCache() { - if (cacheKey != null) { - if (layoutCache == null) { - LayoutCache cache = stringCache.get(cacheKey); - if (cache != null && cache.font.equals(font) && Arrays.equals(cache.text, text)) { - layoutCache = cache; - runs = cache.runs; - runCount = cache.runCount; - flags |= cache.analysis; - } - } - if (layoutCache != null) { - if (copyCache()) { - /* This instance has some property that requires it to - * build its own lines (i.e. wrapping width). Thus, only use - * the runs from the cache (and it needs to make a copy - * before using it as they will be modified). - * Note: the copy of the elements in the array happens in - * reuseRuns(). - */ - if (layoutCache.runs == runs) { - runs = new TextRun[runCount]; - System.arraycopy(layoutCache.runs, 0, runs, 0, runCount); - } - } else { - if (layoutCache.lines != null) { - runs = layoutCache.runs; - runCount = layoutCache.runCount; - flags |= layoutCache.analysis; - lines = layoutCache.lines; - layoutWidth = layoutCache.layoutWidth; - layoutHeight = layoutCache.layoutHeight; - float ascent = lines[0].getBounds().getMinY(); - logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, - layoutWidth, layoutHeight + ascent, 0); - } - } - } - } - } - - private int getLineCount() { - return lines.length; - } - - private int getCharCount() { - if (text != null) return text.length; - int count = 0; - for (int i = 0; i < lines.length; i++) { - count += lines[i].getLength(); - } - return count; - } - - public TextSpan[] getTextSpans() { - return spans; - } - - public PGFont getFont() { - return font; - } - - public int getDirection() { - if ((flags & DIRECTION_LTR) != 0) { - return Bidi.DIRECTION_LEFT_TO_RIGHT; - } - if ((flags & DIRECTION_RTL) != 0) { - return Bidi.DIRECTION_RIGHT_TO_LEFT; - } - if ((flags & DIRECTION_DEFAULT_LTR) != 0) { - return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; - } - if ((flags & DIRECTION_DEFAULT_RTL) != 0) { - return Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT; - } - return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; - } - - public void addTextRun(TextRun run) { - if (runCount + 1 > runs.length) { - TextRun[] newRuns = new TextRun[runs.length + 64]; - System.arraycopy(runs, 0, newRuns, 0, runs.length); - runs = newRuns; - } - runs[runCount++] = run; - } - - private void buildRuns(char[] chars) { - runCount = 0; - if (runs == null) { - int count = Math.max(4, Math.min(chars.length / 16, 16)); - runs = new TextRun[count]; - } - GlyphLayout layout = glyphLayout(); - flags = layout.breakRuns(this, chars, flags); - layout.dispose(); - for (int j = runCount; j < runs.length; j++) { - runs[j] = null; - } - } - - private GlyphLayout glyphLayout() { - return layoutSupplier.get(); - } - - private void shape(TextRun run, char[] chars, GlyphLayout layout) { - FontStrike strike; - PGFont font; - if (spans != null) { - if (spans.length == 0) return; - TextSpan span = run.getTextSpan(); - font = (PGFont)span.getFont(); - if (font == null) { - RectBounds bounds = span.getBounds(); - run.setEmbedded(bounds, span.getText().length()); - return; - } - strike = font.getStrike(IDENTITY); - } else { - font = this.font; - strike = this.strike; - } - - /* init metrics for line breaks for empty lines */ - if (run.getAscent() == 0) { - Metrics m = strike.getMetrics(); - - /* The implementation of the center layoutBounds mode is to assure the - * layout has the same number of pixels above and bellow the cap - * height. - */ - if ((flags & BOUNDS_MASK) == BOUNDS_CENTER) { - float ascent = m.getAscent(); - /* Segoe UI has a very large internal leading area, applying the - * center layoutBounds heuristics on it would result in several pixels - * being added to the descent. The final results would be - * overly large and visually unappealing. The fix is to reduce - * the ascent before applying the algorithm. */ - if (font.getFamilyName().equals("Segoe UI")) { - ascent *= 0.80; - } - ascent = (int)(ascent-0.75); - float descent = (int)(m.getDescent()+0.75); - float leading = (int)(m.getLineGap()+0.75); - float capHeight = (int)(m.getCapHeight()+0.75); - float topPadding = -ascent - capHeight; - if (topPadding > descent) { - descent = topPadding; - } else { - ascent += (topPadding - descent); - } - run.setMetrics(ascent, descent, leading); - } else { - run.setMetrics(m.getAscent(), m.getDescent(), m.getLineGap()); - } - } - - if (run.isTab()) return; - if (run.isLinebreak()) return; - if (run.getGlyphCount() > 0) return; - if (run.isComplex()) { - /* Use GlyphLayout to shape complex text */ - layout.layout(run, font, strike, chars); - } else { - FontResource fr = strike.getFontResource(); - int start = run.getStart(); - int length = run.getLength(); - - /* No glyph layout required */ - if (layoutCache == null) { - float fontSize = strike.getSize(); - CharToGlyphMapper mapper = fr.getGlyphMapper(); - - /* The text contains complex and non-complex runs */ - int[] glyphs = new int[length]; - mapper.charsToGlyphs(start, length, chars, glyphs); - float[] positions = new float[(length + 1) << 1]; - float xadvance = 0; - for (int i = 0; i < length; i++) { - float width = fr.getAdvance(glyphs[i], fontSize); - positions[i<<1] = xadvance; - //yadvance always zero - xadvance += width; - } - positions[length<<1] = xadvance; - run.shape(length, glyphs, positions, null); - } else { - - /* The text only contains non-complex runs, all the glyphs and - * advances are stored in the shapeCache */ - if (!layoutCache.valid) { - float fontSize = strike.getSize(); - CharToGlyphMapper mapper = fr.getGlyphMapper(); - mapper.charsToGlyphs(start, length, chars, layoutCache.glyphs, start); - int end = start + length; - float width = 0; - for (int i = start; i < end; i++) { - float adv = fr.getAdvance(layoutCache.glyphs[i], fontSize); - layoutCache.advances[i] = adv; - width += adv; - } - run.setWidth(width); - } - run.shape(length, layoutCache.glyphs, layoutCache.advances); - } - } - } - - private TextLine createLine(int start, int end, int startOffset, float collapsedSpaceWidth) { - int count = end - start + 1; - - assert count > 0 : "number of TextRuns in a TextLine cannot be less than one: " + count; - - TextRun[] lineRuns = new TextRun[count]; - if (start < runCount) { - System.arraycopy(runs, start, lineRuns, 0, count); - } - - /* Recompute line width, height, and length (wrapping) */ - float width = 0, ascent = 0, descent = 0, leading = 0; - int length = 0; - for (int i = 0; i < lineRuns.length; i++) { - TextRun run = lineRuns[i]; - width += run.getWidth(); - ascent = Math.min(ascent, run.getAscent()); - descent = Math.max(descent, run.getDescent()); - leading = Math.max(leading, run.getLeading()); - length += run.getLength(); - } - - width -= collapsedSpaceWidth; - - if (width > layoutWidth) layoutWidth = width; - return new TextLine(startOffset, length, lineRuns, - width, ascent, descent, leading); - } - - /** - * Computes the size of the white space trailing a given run. - * - * @param run the run to compute trailing space width for, cannot be {@code null} - * @return the X size of the white space trailing the run - */ - private float computeTrailingSpaceWidth(TextRun run) { - float trailingSpaceWidth = 0; - char[] chars = getText(); - - /* - * As the loop below exits when encountering a non-white space character, - * testing each trailing glyph in turn for white space is safe, as white - * space is always represented with only a single glyph: - */ - - for (int i = run.getGlyphCount() - 1; i >= 0; i--) { - int textOffset = run.getStart() + run.getCharOffset(i); - - if (!Character.isWhitespace(chars[textOffset])) { - break; - } - - trailingSpaceWidth += run.getAdvance(i); - } - - return trailingSpaceWidth; - } - - private void reorderLine(TextLine line) { - TextRun[] runs = line.getRuns(); - int length = runs.length; - if (length > 0 && runs[length - 1].isLinebreak()) { - length--; - } - if (length < 2) return; - byte[] levels = new byte[length]; - for (int i = 0; i < length; i++) { - levels[i] = runs[i].getLevel(); - } - Bidi.reorderVisually(levels, 0, runs, 0, length); - } - - private char[] getText() { - if (text == null) { - int count = 0; - for (int i = 0; i < spans.length; i++) { - count += spans[i].getText().length(); - } - text = new char[count]; - int offset = 0; - for (int i = 0; i < spans.length; i++) { - String string = spans[i].getText(); - int length = string.length(); - string.getChars(0, length, text, offset); - offset += length; - } - } - return text; - } - - private boolean isSimpleLayout() { - int textAlignment = flags & ALIGN_MASK; - boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; - int mask = FLAGS_HAS_BIDI | FLAGS_HAS_COMPLEX; - return (flags & mask) == 0 && !justify; - } - - private boolean isMirrored() { - boolean mirrored = false; - switch (flags & DIRECTION_MASK) { - case DIRECTION_RTL: mirrored = true; break; - case DIRECTION_LTR: mirrored = false; break; - case DIRECTION_DEFAULT_LTR: - case DIRECTION_DEFAULT_RTL: - mirrored = (flags & FLAGS_RTL_BASE) != 0; - } - return mirrored; - } - - private float getMirroringWidth() { - /* The text node in the scene layer is mirrored based on - * result of computeLayoutBounds. The coordinate translation - * in text layout has to be based on the same width. - */ - return wrapWidth != 0 ? wrapWidth : layoutWidth; - } - - private void reuseRuns() { - /* The runs list is always accessed by the same thread (as TextLayout - * is not thread safe) thus it can be modified at any time, but the - * elements inside of the list are shared among threads and cannot be - * modified. Each reused element has to be cloned.*/ - runCount = 0; - int index = 0; - while (index < runs.length) { - TextRun run = runs[index]; - if (run == null) break; - runs[index] = null; - index++; - runs[runCount++] = run = run.unwrap(); - - if (run.isSplit()) { - run.merge(null); /* unmark split */ - while (index < runs.length) { - TextRun nextRun = runs[index]; - if (nextRun == null) break; - run.merge(nextRun); - runs[index] = null; - index++; - if (nextRun.isSplitLast()) break; - } - } - } - } - - private float getTabAdvance() { - float spaceAdvance = 0; - if (spans != null) { - /* Rich text case - use the first font (for now) */ - for (int i = 0; i < spans.length; i++) { - TextSpan span = spans[i]; - PGFont font = (PGFont)span.getFont(); - if (font != null) { - FontStrike strike = font.getStrike(IDENTITY); - spaceAdvance = strike.getCharAdvance(' '); - break; - } - } - } else { - spaceAdvance = strike.getCharAdvance(' '); - } - return tabSize * spaceAdvance; - } - - /* - * The way JavaFX lays out text: - * - * JavaFX distinguishes between soft wraps and hard wraps. Soft wraps - * occur when a wrap width has been set and the text requires wrapping - * to stay within the set wrap width. Hard wraps are explicitly part of - * the text in the form of line feeds (LF) and carriage returns (CR). - * Hard wrapping considers a singular LF or CR, or the combination of - * CR+LF (or LF+CR) as a single wrap location. Hard wrapping also occurs - * between TextSpans when multiple TextSpans were supplied (for wrapping - * purposes, there is no difference between two TextSpans and a single - * TextSpan where the text was concatenated with a line break in between). - * - * Soft wrapping occurs when a wrap width has been set. This occurs at - * the first character that does not fit. - * - * - If that character is not a white space, the break is set immediately - * after the first white space encountered before that character - * - If there is no white space before the preferred break character, the - * break is done at the first character that does not fit (the wrap - * then occurs in the middle of a (long) word) - * - If the preferred break character is white space, and it is followed by - * more white space, the break is moved to the end of the white space (thus - * a break in white space always occurs at first non white space character - * following a white space sequence) - * - * White space collapsing: - * - * Only white space that is present at soft wrapped locations is collapsed to - * zero. Any other white space is preserved. This includes white space between - * words, leading and trailing white space, and white space around hard wrapped - * locations. - * - * Alignment: - * - * The alignment calculation only looks at the width of all the significant - * characters in each line. Significant characters are any non white space - * characters and any white space that has been preserved (white space that wasn't - * collapsed due to soft wrapping). - * - * Alignment does not take text effects, such as strike through and underline, into - * account. This means that such effects can appear unaligned. Trailing spaces at a - * soft wrap location (that are underlined for example), may show the underline go - * outside the logical bounds of the text. - * - * Example, where indicates a soft wrap location, and is a line feed: - * - * " The quick brown fox jumps over the lazy dog " - * - * Would be rendered as (left aligned): - * - * " The quick" - * "brown fox jumps" - * "over the " - * " lazy dog " - * - * The alignment calculation uses the above bounds indicated by the double - * quotes, and so right aligned text would look like: - * - * " The quick" - * "brown fox jumps" - * "over the " - * " lazy dog " - * - * Note that only the white space at the soft wrap locations is collapsed. - * In all other locations the space was preserved (the space between words - * where no soft wrap occurred, the leading and trailing space, and the - * space around the hard wrapped location). - * - * Text effects have no effect on the alignment, and so with underlining on - * the right aligned text would look like: - * - * "___The___quick_" (one collapsed space becomes visible here) - * "brown_fox_jumps__" (two collapsed spaces become visible here) - * "over_the_" - * "_lazy_dog___" - * - * Note that text alignment has not changed at all, but the bounds are exceeded - * in some locations to allow for the underline. Controls displaying such texts - * will likely clip the underlined parts exceeding the bounds. - * - * Users wishing to mitigate some of these perhaps surprising results can ensure - * they use trimmed texts, and avoid the use of line breaks, or at least ensure - * that line breaks are not preceded or succeeded by white space (activating - * line wrapping is not equivalent to collapsing any consecutive white space - * no matter where it occurs). - */ - - private void layout() { - /* Try the cache */ - initCache(); - - /* Whole layout retrieved from the cache */ - if (lines != null) return; - char[] chars = getText(); - - /* runs and runCount are set in reuseRuns or buildRuns */ - if ((flags & FLAGS_ANALYSIS_VALID) != 0 && isSimpleLayout()) { - reuseRuns(); - } else { - buildRuns(chars); - } - - GlyphLayout layout = null; - if ((flags & (FLAGS_HAS_COMPLEX)) != 0) { - layout = glyphLayout(); - } - - float tabAdvance = 0; - if ((flags & FLAGS_HAS_TABS) != 0) { - tabAdvance = getTabAdvance(); - } - - BreakIterator boundary = null; - if (wrapWidth > 0) { - if ((flags & (FLAGS_HAS_COMPLEX | FLAGS_HAS_CJK)) != 0) { - boundary = BreakIterator.getLineInstance(); - boundary.setText(new CharArrayIterator(chars)); - } - } - int textAlignment = flags & ALIGN_MASK; - - /* Optimize simple case: reuse the glyphs and advances as long as the - * text and font are the same. - * The simple case is no bidi, no complex, no justify, no features. - */ - - if (isSimpleLayout()) { - if (layoutCache == null) { - layoutCache = new LayoutCache(); - layoutCache.glyphs = new int[chars.length]; - layoutCache.advances = new float[chars.length]; - } - } else { - layoutCache = null; - } - - float lineWidth = 0; - int startIndex = 0; - int startOffset = 0; - ArrayList linesList = new ArrayList<>(); - for (int i = 0; i < runCount; i++) { - TextRun run = runs[i]; - shape(run, chars, layout); - if (run.isTab()) { - float tabStop = ((int)(lineWidth / tabAdvance) +1) * tabAdvance; - run.setWidth(tabStop - lineWidth); - } - - float runWidth = run.getWidth(); - if (wrapWidth > 0 && lineWidth + runWidth > wrapWidth && !run.isLinebreak()) { - - /* Find offset of the first character that does not fit on the line */ - int hitOffset = run.getStart() + run.getWrapIndex(wrapWidth - lineWidth); - - /* - * Only keep white spaces (not tabs) in the current run to avoid - * dealing with unshaped runs. - * - * If the run is a tab, the run will be always of length 1 (see - * buildRuns()). As there is no "next" character that can be selected - * as the wrap index in this run, the white space skipping logic - * below won't skip tabs. - */ - - int offset = hitOffset; - int runEnd = run.getEnd(); - - // Don't take white space into account at the preferred wrap index: - while (offset + 1 < runEnd && Character.isWhitespace(chars[offset])) { - offset++; - } - - /* Find the break opportunity */ - int breakOffset = offset; - if (boundary != null) { - /* Use Java BreakIterator when complex script are present */ - breakOffset = boundary.isBoundary(offset) || chars[offset] == '\t' ? offset : boundary.preceding(offset); - } else { - /* Simple break strategy for latin text (Performance) */ - boolean currentChar = Character.isWhitespace(chars[breakOffset]); - while (breakOffset > startOffset) { - boolean previousChar = Character.isWhitespace(chars[breakOffset - 1]); - if (!currentChar && previousChar) break; - currentChar = previousChar; - breakOffset--; - } - } - - /* Never break before the line start offset */ - if (breakOffset < startOffset) breakOffset = startOffset; - - /* Find the run that contains the break offset */ - int breakRunIndex = startIndex; - TextRun breakRun = null; - while (breakRunIndex < runCount) { - breakRun = runs[breakRunIndex]; - if (breakRun.getEnd() > breakOffset) break; - breakRunIndex++; - } - - /* No line breaks between hit offset and line start offset. - * Try character wrapping mode at the hit offset. - */ - if (breakOffset == startOffset) { - breakRun = run; - breakRunIndex = i; - breakOffset = hitOffset; - } - - int breakOffsetInRun = breakOffset - breakRun.getStart(); - /* Wrap the entire run to the next (only if it is not the first - * run of the line). - */ - if (breakOffsetInRun == 0 && breakRunIndex != startIndex) { - i = breakRunIndex - 1; - } else { - i = breakRunIndex; - - /* The break offset is at the first offset of the first run of the line. - * This happens when the wrap width is smaller than the width require - * to show the first character for the line. - */ - if (breakOffsetInRun == 0) { - breakOffsetInRun++; - } - if (breakOffsetInRun < breakRun.getLength()) { - if (runCount >= runs.length) { - TextRun[] newRuns = new TextRun[runs.length + 64]; - System.arraycopy(runs, 0, newRuns, 0, i + 1); - System.arraycopy(runs, i + 1, newRuns, i + 2, runs.length - i - 1); - runs = newRuns; - } else { - System.arraycopy(runs, i + 1, runs, i + 2, runCount - i - 1); - } - runs[i + 1] = breakRun.split(breakOffsetInRun); - if (breakRun.isComplex()) { - shape(breakRun, chars, layout); - } - runCount++; - } - } - - /* No point marking the last run of a line a softbreak */ - if (i + 1 < runCount && !runs[i + 1].isLinebreak()) { - run = runs[i]; - run.setSoftbreak(); - flags |= FLAGS_WRAPPED; - - // Tabs should preserve width - - /* - * Due to contextual forms (arabic) it is possible this line - * is still too big since the splitting of the arabic run - * changes the shape of boundary glyphs. For now the - * implementation has opted to have the appropriate - * initial/final shapes and allow those glyphs to - * potentially overlap the wrapping width, rather than use - * the medial form within the wrappingWidth. A better place - * to solve this would be TextRun#getWrapIndex - but its TBD - * there too. - */ - } - } - - lineWidth += runWidth; - if (run.isBreak()) { - TextLine line = createLine(startIndex, i, startOffset, computeTrailingSpaceWidth(runs[i])); - linesList.add(line); - startIndex = i + 1; - startOffset += line.getLength(); - lineWidth = 0; - } - } - if (layout != null) layout.dispose(); - - linesList.add(createLine(startIndex, runCount - 1, startOffset, 0)); - lines = new TextLine[linesList.size()]; - linesList.toArray(lines); - - float fullWidth = wrapWidth > 0 ? wrapWidth : layoutWidth; // layoutWidth = widest line, wrapWidth is user set - float lineY = 0; - float align; - if (isMirrored()) { - align = 1; /* Left and Justify */ - if (textAlignment == ALIGN_RIGHT) align = 0; - } else { - align = 0; /* Left and Justify */ - if (textAlignment == ALIGN_RIGHT) align = 1; - } - if (textAlignment == ALIGN_CENTER) align = 0.5f; - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - int lineStart = line.getStart(); - RectBounds bounds = line.getBounds(); - - /* Center and right alignment */ - float unusedWidth = fullWidth - bounds.getWidth(); - float lineX = unusedWidth * align; - line.setAlignment(lineX); - - /* Justify */ - boolean justify = wrapWidth > 0 && textAlignment == ALIGN_JUSTIFY; - if (justify) { - TextRun[] lineRuns = line.getRuns(); - int lineRunCount = lineRuns.length; - if (lineRunCount > 0 && lineRuns[lineRunCount - 1].isSoftbreak()) { - /* count white spaces but skipping trailings whitespaces */ - int lineEnd = lineStart + line.getLength(); - int wsCount = 0; - boolean hitChar = false; - for (int j = lineEnd - 1; j >= lineStart; j--) { - if (!hitChar && chars[j] != ' ') hitChar = true; - if (hitChar && chars[j] == ' ') wsCount++; - } - if (wsCount != 0) { - float inc = unusedWidth / wsCount; - done: - for (int j = 0; j < lineRunCount; j++) { - TextRun textRun = lineRuns[j]; - int runStart = textRun.getStart(); - int runEnd = textRun.getEnd(); - for (int k = runStart; k < runEnd; k++) { - // TODO kashidas - if (chars[k] == ' ') { - textRun.justify(k - runStart, inc); - if (--wsCount == 0) break done; - } - } - } - lineX = 0; - line.setAlignment(lineX); - line.setWidth(fullWidth); - } - } - } - - if ((flags & FLAGS_HAS_BIDI) != 0) { - reorderLine(line); - } - - computeSideBearings(line); - - /* Set run location */ - float runX = lineX; - TextRun[] lineRuns = line.getRuns(); - for (int j = 0; j < lineRuns.length; j++) { - TextRun run = lineRuns[j]; - run.setLocation(runX, lineY); - run.setLine(line); - runX += run.getWidth(); - } - if (i + 1 < lines.length) { - lineY = Math.max(lineY, lineY + bounds.getHeight() + spacing); - } else { - lineY += (bounds.getHeight() - line.getLeading()); - } - } - float ascent = lines[0].getBounds().getMinY(); - layoutHeight = lineY; - logicalBounds = logicalBounds.deriveWithNewBounds(0, ascent, 0, layoutWidth, - layoutHeight + ascent, 0); - - - if (layoutCache != null) { - if (cacheKey != null && !layoutCache.valid && !copyCache()) { - /* After layoutCache is added to the stringCache it can be - * accessed by multiple threads. All the data in it must - * be immutable. See copyCache() for the cases where the entire - * layout is immutable. - */ - layoutCache.font = font; - layoutCache.text = text; - layoutCache.runs = runs; - layoutCache.runCount = runCount; - layoutCache.lines = lines; - layoutCache.layoutWidth = layoutWidth; - layoutCache.layoutHeight = layoutHeight; - layoutCache.analysis = flags & ANALYSIS_MASK; - synchronized (CACHE_SIZE_LOCK) { - int charCount = chars.length; - if (cacheSize + charCount > MAX_CACHE_SIZE) { - stringCache.clear(); - cacheSize = 0; - } - stringCache.put(cacheKey, layoutCache); - cacheSize += charCount; - } - } - layoutCache.valid = true; - } - } - - @Override - public BaseBounds getVisualBounds(int type) { - ensureLayout(); - - /* Not defined for rich text */ - if (strike == null) { - return null; - } - - boolean underline = (type & TYPE_UNDERLINE) != 0; - boolean hasUnderline = (flags & FLAGS_CACHED_UNDERLINE) != 0; - boolean strikethrough = (type & TYPE_STRIKETHROUGH) != 0; - boolean hasStrikethrough = (flags & FLAGS_CACHED_STRIKETHROUGH) != 0; - if (visualBounds != null && underline == hasUnderline - && strikethrough == hasStrikethrough) { - /* Return last cached value */ - return visualBounds; - } - - flags &= ~(FLAGS_CACHED_STRIKETHROUGH | FLAGS_CACHED_UNDERLINE); - if (underline) flags |= FLAGS_CACHED_UNDERLINE; - if (strikethrough) flags |= FLAGS_CACHED_STRIKETHROUGH; - visualBounds = new RectBounds(); - - float xMin = Float.POSITIVE_INFINITY; - float yMin = Float.POSITIVE_INFINITY; - float xMax = Float.NEGATIVE_INFINITY; - float yMax = Float.NEGATIVE_INFINITY; - float bounds[] = new float[4]; - FontResource fr = strike.getFontResource(); - Metrics metrics = strike.getMetrics(); - float size = strike.getSize(); - for (int i = 0; i < lines.length; i++) { - TextLine line = lines[i]; - TextRun[] runs = line.getRuns(); - for (int j = 0; j < runs.length; j++) { - TextRun run = runs[j]; - Point2D pt = run.getLocation(); - if (run.isLinebreak()) continue; - int glyphCount = run.getGlyphCount(); - for (int gi = 0; gi < glyphCount; gi++) { - int gc = run.getGlyphCode(gi); - if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { - fr.getGlyphBoundingBox(run.getGlyphCode(gi), size, bounds); - if (bounds[X_MIN_INDEX] != bounds[X_MAX_INDEX]) { - float glyphX = pt.x + run.getPosX(gi); - float glyphY = pt.y + run.getPosY(gi); - float glyphMinX = glyphX + bounds[X_MIN_INDEX]; - float glyphMinY = glyphY - bounds[Y_MAX_INDEX]; - float glyphMaxX = glyphX + bounds[X_MAX_INDEX]; - float glyphMaxY = glyphY - bounds[Y_MIN_INDEX]; - if (glyphMinX < xMin) xMin = glyphMinX; - if (glyphMinY < yMin) yMin = glyphMinY; - if (glyphMaxX > xMax) xMax = glyphMaxX; - if (glyphMaxY > yMax) yMax = glyphMaxY; - } - } - } - if (underline) { - float underlineMinX = pt.x; - float underlineMinY = pt.y + metrics.getUnderLineOffset(); - float underlineMaxX = underlineMinX + run.getWidth(); - float underlineMaxY = underlineMinY + metrics.getUnderLineThickness(); - if (underlineMinX < xMin) xMin = underlineMinX; - if (underlineMinY < yMin) yMin = underlineMinY; - if (underlineMaxX > xMax) xMax = underlineMaxX; - if (underlineMaxY > yMax) yMax = underlineMaxY; - } - if (strikethrough) { - float strikethroughMinX = pt.x; - float strikethroughMinY = pt.y + metrics.getStrikethroughOffset(); - float strikethroughMaxX = strikethroughMinX + run.getWidth(); - float strikethroughMaxY = strikethroughMinY + metrics.getStrikethroughThickness(); - if (strikethroughMinX < xMin) xMin = strikethroughMinX; - if (strikethroughMinY < yMin) yMin = strikethroughMinY; - if (strikethroughMaxX > xMax) xMax = strikethroughMaxX; - if (strikethroughMaxY > yMax) yMax = strikethroughMaxY; - } - } - } - - if (xMin < xMax && yMin < yMax) { - visualBounds.setBounds(xMin, yMin, xMax, yMax); - } - return visualBounds; - } - - private void computeSideBearings(TextLine line) { - TextRun[] runs = line.getRuns(); - if (runs.length == 0) return; - float bounds[] = new float[4]; - FontResource defaultFontResource = null; - float size = 0; - if (strike != null) { - defaultFontResource = strike.getFontResource(); - size = strike.getSize(); - } - - /* The line lsb is the lsb of the first visual character in the line */ - float lsb = 0; - float width = 0; - lsbdone: - for (int i = 0; i < runs.length; i++) { - TextRun run = runs[i]; - int glyphCount = run.getGlyphCount(); - for (int gi = 0; gi < glyphCount; gi++) { - float advance = run.getAdvance(gi); - /* Skip any leading zero-width glyphs in the line */ - if (advance != 0) { - int gc = run.getGlyphCode(gi); - /* Skip any leading invisible glyphs in the line */ - if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { - FontResource fr = defaultFontResource; - if (fr == null) { - TextSpan span = run.getTextSpan(); - PGFont font = (PGFont)span.getFont(); - /* No need to check font != null (run.glyphCount > 0) */ - size = font.getSize(); - fr = font.getFontResource(); - } - fr.getGlyphBoundingBox(gc, size, bounds); - float glyphLsb = bounds[X_MIN_INDEX]; - lsb = Math.min(0, glyphLsb + width); - run.setLeftBearing(); - break lsbdone; - } - } - width += advance; - } - // tabs - if (glyphCount == 0) { - width += run.getWidth(); - } - } - - /* The line rsb is the rsb of the last visual character in the line */ - float rsb = 0; - width = 0; - rsbdone: - for (int i = runs.length - 1; i >= 0 ; i--) { - TextRun run = runs[i]; - int glyphCount = run.getGlyphCount(); - for (int gi = glyphCount - 1; gi >= 0; gi--) { - float advance = run.getAdvance(gi); - /* Skip any trailing zero-width glyphs in the line */ - if (advance != 0) { - int gc = run.getGlyphCode(gi); - /* Skip any trailing invisible glyphs in the line */ - if (gc != CharToGlyphMapper.INVISIBLE_GLYPH_ID) { - FontResource fr = defaultFontResource; - if (fr == null) { - TextSpan span = run.getTextSpan(); - PGFont font = (PGFont)span.getFont(); - /* No need to check font != null (run.glyphCount > 0) */ - size = font.getSize(); - fr = font.getFontResource(); - } - fr.getGlyphBoundingBox(gc, size, bounds); - float glyphRsb = bounds[X_MAX_INDEX] - advance; - rsb = Math.max(0, glyphRsb - width); - run.setRightBearing(); - break rsbdone; - } - } - width += advance; - } - // tabs - if (glyphCount == 0) { - width += run.getWidth(); - } - } - line.setSideBearings(lsb, rsb); - } -} diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutFactory.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutFactory.java index 213de72eca5..5107b845adc 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutFactory.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/text/PrismTextLayoutFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,30 +25,37 @@ package com.sun.javafx.text; +import com.sun.javafx.font.PrismFontFactory; +import com.sun.javafx.scene.text.TextLayout; import com.sun.javafx.scene.text.TextLayoutFactory; public class PrismTextLayoutFactory implements TextLayoutFactory { - + private static final PrismTextLayoutFactory factory = new PrismTextLayoutFactory(); /* Same strategy as GlyphLayout */ - private static final PrismTextLayout reusableTL = new PrismTextLayout(); + private static final TextLayout reusableTL = factory.createLayout(); private static boolean inUse; private PrismTextLayoutFactory() { } @Override - public com.sun.javafx.scene.text.TextLayout createLayout() { - return new PrismTextLayout(); + public TextLayout createLayout() { + return new PrismTextLayout(PrismFontFactory.cacheLayoutSize) { + @Override + protected GlyphLayout glyphLayout() { + return GlyphLayoutManager.getInstance(); + } + }; } @Override - public com.sun.javafx.scene.text.TextLayout getLayout() { + public TextLayout getLayout() { if (inUse) { - return new PrismTextLayout(); + return createLayout(); } else { synchronized(PrismTextLayoutFactory.class) { if (inUse) { - return new PrismTextLayout(); + return createLayout(); } else { inUse = true; reusableTL.setAlignment(0); @@ -62,13 +69,12 @@ public com.sun.javafx.scene.text.TextLayout getLayout() { } @Override - public void disposeLayout(com.sun.javafx.scene.text.TextLayout layout) { + public void disposeLayout(TextLayout layout) { if (layout == reusableTL) { inUse = false; } } - private static final PrismTextLayoutFactory factory = new PrismTextLayoutFactory(); public static PrismTextLayoutFactory getFactory() { return factory; } diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java index 4aa3195cc2a..05a61ca8337 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontResource.java @@ -111,7 +111,6 @@ public boolean isItalic() { public float getAdvance(int gc, float size) { // +1 for bold fonts return isBold() ? size + StubFontMetrics.BOLD_FONT_EXTRA_WIDTH : size; - //StubTextLayout.p("gx=0x%04X size=%f", gc, size); } // returns [xmin, ymin, xmax, ymax] @@ -120,10 +119,6 @@ public float[] getGlyphBoundingBox(int gc, float size, float[] b) { if (b == null || b.length < 4) { b = new float[4]; } - // TODO for non-printable return all 0's? - if (gc < 0x20) { - StubTextLayout.p("gc=%04X", gc); - } float xmin = 0.0f; float ymin = 0.0f; @@ -154,7 +149,6 @@ public CharToGlyphMapper getGlyphMapper() { @Override public Map> getStrikeMap() { - StubTextLayout.p(""); return null; } @@ -170,13 +164,11 @@ public FontStrike getStrike(float size, BaseTransform t, int aaMode) { @Override public Object getPeer() { - StubTextLayout.p(""); return null; } @Override public void setPeer(Object peer) { - StubTextLayout.p(""); } @Override diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java index fba00fd4341..62c60f2e91b 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubFontStrike.java @@ -81,13 +81,11 @@ public Metrics getMetrics() { @Override public Glyph getGlyph(char symbol) { - StubTextLayout.p(""); return null; } @Override public Glyph getGlyph(int glyphCode) { - StubTextLayout.p(""); return null; } @@ -108,7 +106,6 @@ public float getCharAdvance(char ch) { @Override public Shape getOutline(GlyphList gl, BaseTransform transform) { - StubTextLayout.p(""); return null; } } diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java index a5cc47307b3..32d0da5c98b 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java @@ -24,6 +24,8 @@ */ package test.com.sun.javafx.pgstub; +import com.sun.javafx.font.CharToGlyphMapper; +import com.sun.javafx.font.FontResource; import com.sun.javafx.font.FontStrike; import com.sun.javafx.font.PGFont; import com.sun.javafx.text.GlyphLayout; @@ -41,8 +43,27 @@ public void dispose() { } @Override - public void layout(TextRun run, PGFont font, FontStrike strike, char[] text) { - // TODO - StubTextLayout.p(""); + public void layout(TextRun run, PGFont font, FontStrike strike, char[] chars) { + FontResource fr = strike.getFontResource(); + int start = run.getStart(); + int length = run.getLength(); + + // simple case taken from PrismTextLayout.shape() + float fontSize = strike.getSize(); + CharToGlyphMapper mapper = fr.getGlyphMapper(); + + /* The text contains complex and non-complex runs */ + int[] glyphs = new int[length]; + mapper.charsToGlyphs(start, length, chars, glyphs); + float[] positions = new float[(length + 1) << 1]; + float xadvance = 0; + for (int i = 0; i < length; i++) { + float width = fr.getAdvance(glyphs[i], fontSize); + positions[i << 1] = xadvance; + //yadvance always zero + xadvance += width; + } + positions[length << 1] = xadvance; + run.shape(length, glyphs, positions, 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 eb6971a06c5..b538cadca63 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 @@ -25,25 +25,21 @@ package test.com.sun.javafx.pgstub; -import com.sun.javafx.text.PrismTextLayoutBase; +import com.sun.javafx.text.GlyphLayout; +import com.sun.javafx.text.PrismTextLayout; /** * Same as PrismTextLayout but with stubbed out fonts. */ -public class StubTextLayout extends PrismTextLayoutBase { +public class StubTextLayout extends PrismTextLayout { public static final boolean DEBUG = true; public StubTextLayout() { - super(256, StubGlyphLayout::new); + super(256); } - public static void p(String fmt, Object... args) { - if (DEBUG) { - StackTraceElement s = new Throwable().getStackTrace()[1]; - String name = s.getClassName(); - int ix = name.lastIndexOf('.'); - name = (ix < 0) ? name : name.substring(ix + 1); - System.out.println("🐞 " + name + "." + s.getMethodName() + " " + String.format(fmt, args)); - } + @Override + protected GlyphLayout glyphLayout() { + return new StubGlyphLayout(); } } 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 index b0435dfa60b..6c5a3d7974b 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, 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 @@ -32,12 +32,13 @@ 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; import com.sun.javafx.scene.text.TextLayout.Hit; import com.sun.javafx.scene.text.TextSpan; -import com.sun.javafx.text.PrismTextLayout; +import com.sun.javafx.text.PrismTextLayoutFactory; public class TextHitInfoTest { - private final PrismTextLayout layout = new PrismTextLayout(); + private final TextLayout layout = PrismTextLayoutFactory.getFactory().createLayout(); private final PGFont arialFont = (PGFont) FontHelper.getNativeFont(Font.font("Arial", 12)); record TestSpan(String text, Object font) implements TextSpan { diff --git a/tests/system/src/test/java/test/com/sun/javafx/text/TextLayoutTest.java b/tests/system/src/test/java/test/com/sun/javafx/text/TextLayoutTest.java index 50be840995f..456bc735ae2 100644 --- a/tests/system/src/test/java/test/com/sun/javafx/text/TextLayoutTest.java +++ b/tests/system/src/test/java/test/com/sun/javafx/text/TextLayoutTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -42,9 +42,10 @@ import com.sun.javafx.geom.RectBounds; import com.sun.javafx.scene.text.FontHelper; import com.sun.javafx.scene.text.GlyphList; +import com.sun.javafx.scene.text.TextLayout; import com.sun.javafx.scene.text.TextLine; import com.sun.javafx.scene.text.TextSpan; -import com.sun.javafx.text.PrismTextLayout; +import com.sun.javafx.text.PrismTextLayoutFactory; import com.sun.javafx.text.TextRun; public class TextLayoutTest { @@ -52,7 +53,7 @@ public class TextLayoutTest { private static final String D = "\u0907"; // Devanagari complex private static final String T = "\u0E34"; // Thai complex - private final PrismTextLayout layout = new PrismTextLayout(); + private final TextLayout layout = PrismTextLayoutFactory.getFactory().createLayout(); private final PGFont arialFont = (PGFont) FontHelper.getNativeFont(Font.font("Arial", 12)); private final PGFont tahomaFont = (PGFont) FontHelper.getNativeFont(Font.font("Tahoma", 12)); @@ -73,7 +74,7 @@ public RectBounds getBounds() { } } - private void setContent(PrismTextLayout layout, Object... content) { + private void setContent(TextLayout layout, Object... content) { int count = content.length / 2; TextSpan[] spans = new TextSpan[count]; int i = 0; From ad5f5146c3ca68269581a859669722d21001259f Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Fri, 24 Jan 2025 14:00:50 -0800 Subject: [PATCH 14/15] better test --- .../javafx/scene/control/ListViewTest.java | 72 ++++++++++++------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java index 0543867281f..e262338e373 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/ListViewTest.java @@ -1143,71 +1143,89 @@ public void test_rt_35395_notFixedCellSize() { test_rt_35395(false); } - private int rt_35395_counter; + private class Counter { + public int updateCount; - private void test_rt_35395(boolean useFixedCellSize) { - rt_35395_counter = 0; + public static void reset(List items) { + for (Counter c : items) { + c.updateCount = 0; + } + } - ObservableList items = FXCollections.observableArrayList(); + // verifies problem of JDK-8091726: that an update() method is not called more than once + public static void verify(List items) { + for (int i = 0; i < items.size(); i++) { + Counter c = items.get(i); + int count = c.updateCount; + c.updateCount = 0; + assertTrue(c.updateCount < 2, "index=" + i + " updateCount=" + count); + } + } + } + + // JDK-8091726 + private void test_rt_35395(boolean useFixedCellSize) { + ObservableList items = FXCollections.observableArrayList(); for (int i = 0; i < 20; ++i) { - items.addAll("red", "green", "blue", "purple"); + items.addAll(new Counter(), new Counter(), new Counter(), new Counter()); } - ListView listView = new ListView<>(items); + ListView listView = new ListView<>(items); if (useFixedCellSize) { listView.setFixedCellSize(18); } listView.setCellFactory(lv -> new ListCellShim<>() { @Override - public void updateItem(String color, boolean empty) { - rt_35395_counter += 1; - super.updateItem(color, empty); + public void updateItem(Counter item, boolean empty) { + if (item != null) { + item.updateCount++; + } + super.updateItem(item, empty); setText(null); if (empty) { setGraphic(null); } else { Rectangle rect = new Rectangle(16, 16); - rect.setStyle("-fx-fill: " + color); + rect.setStyle("-fx-fill: red"); setGraphic(rect); } } }); StageLoader sl = new StageLoader(listView); - listView.setPrefHeight(400 / 10.0 * Font.getDefault().getSize()); - Toolkit.getToolkit().firePulse(); Platform.runLater(() -> { - rt_35395_counter = 0; - items.set(10, "yellow"); + Counter.reset(items); + items.set(10, new Counter()); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertEquals(1, rt_35395_counter); - rt_35395_counter = 0; - items.set(30, "yellow"); + Counter.verify(items); + + items.set(30, new Counter()); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertTrue(rt_35395_counter < 7); - rt_35395_counter = 0; + Counter.verify(items); + items.remove(12); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertEquals(useFixedCellSize ? 15 : 10, rt_35395_counter); - rt_35395_counter = 0; - items.add(12, "yellow"); + Counter.verify(items); + + items.add(12, new Counter()); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertEquals(useFixedCellSize ? 15 : 10, rt_35395_counter); - rt_35395_counter = 0; + Counter.verify(items); + listView.scrollTo(5); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertTrue(rt_35395_counter < 30); - rt_35395_counter = 0; + Counter.verify(items); + listView.scrollTo(55); Platform.runLater(() -> { Toolkit.getToolkit().firePulse(); - assertEquals(useFixedCellSize ? 27 : 108, rt_35395_counter); + Counter.verify(items); + sl.dispose(); }); }); From a4a3ebb3a73f04c4bfaaccf559c64f0e1f29e8a3 Mon Sep 17 00:00:00 2001 From: Andy Goryachev Date: Fri, 24 Jan 2025 15:37:10 -0800 Subject: [PATCH 15/15] cleanup --- .../main/java/com/sun/javafx/scene/text/GlyphList.java | 2 +- .../src/main/java/com/sun/javafx/scene/text/TextSpan.java | 2 +- .../main/java/com/sun/javafx/text/PrismTextLayout.java | 8 ++++---- .../src/main/java/javafx/scene/text/Text.java | 2 +- .../java/test/com/sun/javafx/pgstub/StubGlyphLayout.java | 2 +- .../java/test/com/sun/javafx/pgstub/StubTextLayout.java | 3 +-- .../test/java/test/javafx/scene/layout/BaselineTest.java | 1 - 7 files changed, 9 insertions(+), 11 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/GlyphList.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/GlyphList.java index 638e0d9d969..8aa3e0a02c4 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/GlyphList.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/GlyphList.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextSpan.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextSpan.java index b648dba4b1f..0a65a902236 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextSpan.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextSpan.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 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 85e9078d88b..d96401d4e05 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 @@ -65,8 +65,8 @@ public abstract class PrismTextLayout implements TextLayout { private static final Object CACHE_SIZE_LOCK = new Object(); private static int cacheSize = 0; private static final int MAX_STRING_SIZE = 256; - private final int MAX_CACHE_SIZE; + private final int maxCacheSize; private char[] text; private TextSpan[] spans; /* Rich text (null for single font text) */ private PGFont font; /* Single font text (null for rich text) */ @@ -85,7 +85,7 @@ public abstract class PrismTextLayout implements TextLayout { private int tabSize = DEFAULT_TAB_SIZE; public PrismTextLayout(int maxCacheSize) { - MAX_CACHE_SIZE = maxCacheSize; + this.maxCacheSize = maxCacheSize; logicalBounds = new RectBounds(); flags = ALIGN_LEFT; } @@ -142,7 +142,7 @@ public boolean setContent(String text, Object font) { this.font = (PGFont)font; this.strike = ((PGFont)font).getStrike(IDENTITY); this.text = text.toCharArray(); - if (MAX_CACHE_SIZE > 0) { + if (maxCacheSize > 0) { int length = text.length(); if (0 < length && length <= MAX_STRING_SIZE) { cacheKey = text.hashCode() * strike.hashCode(); @@ -1481,7 +1481,7 @@ private void layout() { layoutCache.analysis = flags & ANALYSIS_MASK; synchronized (CACHE_SIZE_LOCK) { int charCount = chars.length; - if (cacheSize + charCount > MAX_CACHE_SIZE) { + if (cacheSize + charCount > maxCacheSize) { stringCache.clear(); cacheSize = 0; } 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 6c83f6413c2..04b9149e8f9 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java index 32d0da5c98b..6ede00e83ba 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubGlyphLayout.java @@ -48,7 +48,7 @@ public void layout(TextRun run, PGFont font, FontStrike strike, char[] chars) { int start = run.getStart(); int length = run.getLength(); - // simple case taken from PrismTextLayout.shape() + // simplified code from PrismTextLayout.shape() float fontSize = strike.getSize(); CharToGlyphMapper mapper = fr.getGlyphMapper(); 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 b538cadca63..113c508e4f1 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 @@ -32,10 +32,9 @@ * Same as PrismTextLayout but with stubbed out fonts. */ public class StubTextLayout extends PrismTextLayout { - public static final boolean DEBUG = true; public StubTextLayout() { - super(256); + super(0); } @Override diff --git a/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/BaselineTest.java b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/BaselineTest.java index 15ef48ed8cf..04d3e5c4695 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/BaselineTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/BaselineTest.java @@ -51,7 +51,6 @@ public void testShapeBaselineAtBottom() { public void testTextBaseline() { Text text = new Text("Graphically"); float size = (float) text.getFont().getSize(); - //assertEquals(size, text.getBaselineOffset(),1e-100); assertTrue(text.getBaselineOffset() > (size / 2.0f)); }