mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
+15
-14
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
-18
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user