fix highlighting and caret behaviour regression near ligature (IDEA-127539)

related to the way ligatures are represented in a glyph vector when Harfbuzz layout engine is used
This commit is contained in:
Dmitry Batrak
2016-05-30 13:19:25 +03:00
parent ac6d3a4b84
commit cd912a7345
2 changed files with 76 additions and 32 deletions
@@ -44,40 +44,41 @@ class ComplexTextFragment extends TextFragment {
float totalWidth = (float)myGlyphVector.getGlyphPosition(numGlyphs).getX();
myCharPositions[numChars - 1] = totalWidth;
int lastCharIndex = -1;
int ligatureStartCharIndex = 0;
float lastX = isRtl ? totalWidth : 0;
float prevX = lastX;
// Here we determine coordinates for boundaries between characters.
// Here we determine coordinates for boundaries between characters.
// They will be used to place caret, last boundary coordinate is also defining the width of text fragment.
//
// We expect these positions to be ordered, so that when caret moves through text characters in some direction, corresponding text
// offsets change monotonously (within the same-directionality fragment).
//
// Special case that we must account for is a ligature, when several adjacent characters are represented as a single glyph.
// In a glyph vector this glyph is associated with the first character, other characters are associated with empty glyphs.
// In a glyph vector this glyph is associated with the first character,
// other characters either don't have an associated glyph, or they are associated with empty glyphs.
// (in RTL case real glyph will be associated with first logical character, i.e. last visual character)
for (int i = 0; i < numGlyphs; i++) {
int visualGlyphIndex = isRtl ? numGlyphs - 1 - i : i;
int charIndex = myGlyphVector.getGlyphCharIndex(visualGlyphIndex);
if (charIndex > lastCharIndex) {
Rectangle2D bounds = myGlyphVector.getGlyphLogicalBounds(visualGlyphIndex).getBounds2D();
if (bounds.isEmpty()) {
for (int j = ligatureStartCharIndex; j <= charIndex; j++) {
setCharPosition(j, prevX + (lastX - prevX) * (j - ligatureStartCharIndex + 1) / (charIndex - ligatureStartCharIndex + 1),
isRtl, numChars);
if (!bounds.isEmpty()) {
if (charIndex > lastCharIndex + 1) {
for (int j = Math.max(0, lastCharIndex); j < charIndex; j++) {
setCharPosition(j, prevX + (lastX - prevX) * (j - lastCharIndex + 1) / (charIndex - lastCharIndex), isRtl, numChars);
}
}
}
else {
float newX = isRtl ? Math.min(lastX, (float)bounds.getMinX()) : Math.max(lastX, (float)bounds.getMaxX());
newX = Math.max(0, Math.min(totalWidth, newX));
ligatureStartCharIndex = lastCharIndex + 1;
for (int j = ligatureStartCharIndex; j <= charIndex; j++) {
setCharPosition(j, newX, isRtl, numChars);
}
setCharPosition(charIndex, newX, isRtl, numChars);
prevX = lastX;
lastX = newX;
lastCharIndex = charIndex;
}
lastCharIndex = charIndex;
}
}
if (lastCharIndex < numChars - 1) {
for (int j = Math.max(0, lastCharIndex); j < numChars - 1; j++) {
setCharPosition(j, prevX + (lastX - prevX) * (j - lastCharIndex + 1) / (numChars - lastCharIndex), isRtl, numChars);
}
}
}
@@ -25,9 +25,11 @@ import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Point2D;
import java.util.Objects;
import java.util.stream.Stream;
import static org.junit.Assert.assertArrayEquals;
import static com.intellij.openapi.editor.impl.AbstractEditorTest.*;
import static org.junit.Assert.assertArrayEquals;
public class ComplexTextFragmentTest {
@Test
@@ -49,7 +51,7 @@ public class ComplexTextFragmentTest {
@Test
public void testLigature() {
assertCaretPositionsForGlyphVector(
glyph(0, 12).glyph(12, 12).glyph(12, 12),
glyph(0, 12).glyph(12, 12).glyph(12, 12), // ICU-style ligature (when all characters get a glyph)
4, 8, 12
);
}
@@ -57,12 +59,36 @@ public class ComplexTextFragmentTest {
@Test
public void testRtlLigature() {
assertCaretPositionsForGlyphVector(
rtl().glyph(0, 0).glyph(0, 0).glyph(0, 12),
rtl().glyph(0, 0).glyph(0, 0).glyph(0, 12), // ICU-style ligature (when all characters get a glyph)
4, 8, 12
);
}
private static void assertCaretPositionsForGlyphVector(GlyphVector gv, int... expectedPositions) {
@Test
public void testHbLigature() {
assertCaretPositionsForGlyphVector(
glyph(0, 12).noGlyph().noGlyph(), // Harfbuzz-style ligature (some characters don't map to glyphs)
4, 8, 12
);
}
@Test
public void testHbRtlLigature() {
assertCaretPositionsForGlyphVector(
rtl().noGlyph().noGlyph().glyph(0, 12), // Harfbuzz-style ligature (some characters don't map to glyphs)
4, 8, 12
);
}
@Test
public void testHbLigatureTwoChars() {
assertCaretPositionsForGlyphVector(
glyph(0, 8).noGlyph(), // Harfbuzz-style ligature (some characters don't map to glyphs)
4, 8
);
}
private static void assertCaretPositionsForGlyphVector(MyGlyphVector gv, int... expectedPositions) {
FontLayoutService.setInstance(new MockFontLayoutService(TEST_CHAR_WIDTH, TEST_LINE_HEIGHT, TEST_DESCENT) {
@NotNull
@Override
@@ -72,8 +98,7 @@ public class ComplexTextFragmentTest {
}
});
try {
// assuming one glyph per character
int length = gv.getNumGlyphs();
int length = gv.getNumChars();
char[] text = new char[length];
ComplexTextFragment fragment = new ComplexTextFragment(text, 0, length, (gv.getLayoutFlags() & GlyphVector.FLAG_RUN_RTL) != 0,
new Font(null), new FontRenderContext(null, false, false));
@@ -89,19 +114,19 @@ public class ComplexTextFragmentTest {
}
private static MyGlyphVector rtl() {
return new MyGlyphVector(true, new int[0], new int[0]);
return new MyGlyphVector(true, new Integer[0], new Integer[0]);
}
private static MyGlyphVector glyph(int xStart, int xEnd) {
return new MyGlyphVector(false, new int[]{xStart}, new int[]{xEnd - xStart});
return new MyGlyphVector(false, new Integer[]{xStart}, new Integer[]{xEnd - xStart});
}
private static class MyGlyphVector extends AbstractMockGlyphVector {
private final boolean myRtl;
private final int[] myGlyphPositions;
private final int[] myGlyphWidths;
private final Integer[] myGlyphPositions;
private final Integer[] myGlyphWidths;
private MyGlyphVector(boolean rtl, int[] glyphPositions, int[] glyphWidths) {
private MyGlyphVector(boolean rtl, Integer[] glyphPositions, Integer[] glyphWidths) {
assertTrue(glyphPositions.length == glyphWidths.length);
myRtl = rtl;
myGlyphPositions = glyphPositions;
@@ -112,26 +137,44 @@ public class ComplexTextFragmentTest {
return new MyGlyphVector(myRtl, ArrayUtil.append(myGlyphPositions, xStart), ArrayUtil.append(myGlyphWidths, xEnd - xStart));
}
private MyGlyphVector noGlyph() {
return new MyGlyphVector(myRtl, ArrayUtil.append(myGlyphPositions, null), ArrayUtil.append(myGlyphWidths, null));
}
@Override
public int getNumGlyphs() {
return (int)Stream.of(myGlyphPositions).filter(Objects::nonNull).count();
}
private int getNumChars() {
return myGlyphPositions.length;
}
@Override
public Point2D getGlyphPosition(int glyphIndex) {
return new Point(glyphIndex == myGlyphPositions.length
? myGlyphPositions[glyphIndex - 1] + myGlyphWidths[glyphIndex - 1]
: myGlyphPositions[glyphIndex], 0);
boolean afterLast = glyphIndex == getNumGlyphs();
int index = getGlyphIndexInArray(glyphIndex - (afterLast ? 1 : 0));
return new Point(myGlyphPositions[index] + (afterLast ? myGlyphWidths[index] : 0), 0);
}
@Override
public Shape getGlyphLogicalBounds(int glyphIndex) {
return new Rectangle(myGlyphPositions[glyphIndex], -TEST_DESCENT, myGlyphWidths[glyphIndex], TEST_LINE_HEIGHT);
int index = getGlyphIndexInArray(glyphIndex);
return new Rectangle(myGlyphPositions[index], -TEST_DESCENT, myGlyphWidths[index], TEST_LINE_HEIGHT);
}
@Override
public int getGlyphCharIndex(int glyphIndex) {
return myRtl ? myGlyphPositions.length - 1 - glyphIndex : glyphIndex;
int index = getGlyphIndexInArray(glyphIndex);
return myRtl ? getNumChars() - 1 - index : index;
}
private int getGlyphIndexInArray(int glyphIndex) {
int index = 0;
for (int i = 0; i < myGlyphPositions.length; i++) {
if (myGlyphPositions[i] != null && index++ == glyphIndex) return i;
}
return -1;
}
@Override