caret positioning inside ligatures

This commit is contained in:
Dmitry Batrak
2015-12-04 10:51:05 +03:00
parent f777d4ce19
commit 2476c54ba5
4 changed files with 321 additions and 106 deletions
@@ -38,20 +38,55 @@ class ComplexTextFragment extends TextFragment {
assert end <= lineChars.length;
assert start < end;
myGlyphVector = FontLayoutService.getInstance().layoutGlyphVector(font, fontRenderContext, lineChars, start, end, isRtl);
int charIndex = 0;
int numChars = end - start;
int numGlyphs = myGlyphVector.getNumGlyphs();
for (int i = 0; i <= numGlyphs; i++) {
int newCharIndex = i == numGlyphs ? end - start :
isRtl ? (end - start - 1 - myGlyphVector.getGlyphCharIndex(i)) : myGlyphVector.getGlyphCharIndex(i);
if (newCharIndex > charIndex) {
float x = (float)myGlyphVector.getGlyphPosition(i).getX();
for (int j = charIndex; j < newCharIndex; j++) {
myCharPositions[j] = x;
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.
// 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 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);
}
}
charIndex = newCharIndex;
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);
}
prevX = lastX;
lastX = newX;
}
lastCharIndex = charIndex;
}
}
}
private void setCharPosition(int logicalCharIndex, float x, boolean isRtl, int numChars) {
int charPosition = isRtl ? numChars - logicalCharIndex - 2 : logicalCharIndex;
if (charPosition >= 0 && charPosition < numChars - 1) {
myCharPositions[charPosition] = x;
}
}
@Override
public void draw(Graphics2D g, float x, float y, int startColumn, int endColumn) {
@@ -0,0 +1,142 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.editor.impl.view;
import com.intellij.testFramework.AbstractMockGlyphVector;
import com.intellij.testFramework.MockFontLayoutService;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Point2D;
import static org.junit.Assert.assertArrayEquals;
import static com.intellij.openapi.editor.impl.AbstractEditorTest.*;
public class ComplexTextFragmentTest {
@Test
public void testSimpleText() {
assertCaretPositionsForGlyphVector(
glyph(0, 10).glyph(10, 10).glyph(20, 10),
10, 20, 30
);
}
@Test
public void testSimpleRtlText() {
assertCaretPositionsForGlyphVector(
rtl().glyph(0, 10).glyph(10, 10).glyph(20, 10),
10, 20, 30
);
}
@Test
public void testLigature() {
assertCaretPositionsForGlyphVector(
glyph(0, 12).glyph(12, 0).glyph(12, 0),
4, 8, 12
);
}
@Test
public void testRtlLigature() {
assertCaretPositionsForGlyphVector(
rtl().glyph(0, 0).glyph(0, 0).glyph(0, 12),
4, 8, 12
);
}
private static void assertCaretPositionsForGlyphVector(GlyphVector gv, int... expectedPositions) {
FontLayoutService.setInstance(new MockFontLayoutService(TEST_CHAR_WIDTH, TEST_LINE_HEIGHT, TEST_DESCENT) {
@NotNull
@Override
public GlyphVector layoutGlyphVector(@NotNull Font font, @NotNull FontRenderContext fontRenderContext, @NotNull char[] chars,
int start, int end, boolean isRtl) {
return gv;
}
});
try {
// assuming one glyph per character
int length = gv.getNumGlyphs();
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));
int[] charPositions = new int[length];
for (int i = 0; i < length; i++) {
charPositions[i] = (int)fragment.visualColumnToX(0, i + 1);
}
assertArrayEquals(expectedPositions, charPositions);
}
finally {
FontLayoutService.setInstance(null);
}
}
private static MyGlyphVector rtl() {
return new MyGlyphVector(true, new int[0], new int[0]);
}
private static MyGlyphVector glyph(int position, int width) {
return new MyGlyphVector(false, new int[]{position}, new int[]{width});
}
private static class MyGlyphVector extends AbstractMockGlyphVector {
private final boolean myRtl;
private final int[] myGlyphPositions;
private final int[] myGlyphWidths;
private MyGlyphVector(boolean rtl, int[] glyphPositions, int[] glyphWidths) {
assertTrue(glyphPositions.length == glyphWidths.length);
myRtl = rtl;
myGlyphPositions = glyphPositions;
myGlyphWidths = glyphWidths;
}
private MyGlyphVector glyph(int position, int width) {
return new MyGlyphVector(myRtl, ArrayUtil.append(myGlyphPositions, position), ArrayUtil.append(myGlyphWidths, width));
}
@Override
public int getNumGlyphs() {
return myGlyphPositions.length;
}
@Override
public Point2D getGlyphPosition(int glyphIndex) {
return new Point(glyphIndex == myGlyphPositions.length
? myGlyphPositions[glyphIndex - 1] + myGlyphWidths[glyphIndex - 1]
: myGlyphPositions[glyphIndex], 0);
}
@Override
public Shape getGlyphLogicalBounds(int glyphIndex) {
return new Rectangle(myGlyphPositions[glyphIndex], 0, myGlyphWidths[glyphIndex], TEST_LINE_HEIGHT - TEST_DESCENT);
}
@Override
public int getGlyphCharIndex(int glyphIndex) {
return myRtl ? myGlyphPositions.length - 1 - glyphIndex : glyphIndex;
}
@Override
public int getLayoutFlags() {
return myRtl ? GlyphVector.FLAG_RUN_RTL : 0;
}
}
}
@@ -0,0 +1,133 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.testFramework;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphJustificationInfo;
import java.awt.font.GlyphMetrics;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
public abstract class AbstractMockGlyphVector extends GlyphVector {
@Override
public Font getFont() {
throw new UnsupportedOperationException();
}
@Override
public FontRenderContext getFontRenderContext() {
throw new UnsupportedOperationException();
}
@Override
public void performDefaultLayout() {
throw new UnsupportedOperationException();
}
@Override
public int getNumGlyphs() {
throw new UnsupportedOperationException();
}
@Override
public int getGlyphCode(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public int[] getGlyphCodes(int beginGlyphIndex, int numEntries, int[] codeReturn) {
throw new UnsupportedOperationException();
}
@Override
public Rectangle2D getLogicalBounds() {
throw new UnsupportedOperationException();
}
@Override
public Rectangle2D getVisualBounds() {
throw new UnsupportedOperationException();
}
@Override
public Shape getOutline() {
throw new UnsupportedOperationException();
}
@Override
public Shape getOutline(float x, float y) {
throw new UnsupportedOperationException();
}
@Override
public Shape getGlyphOutline(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public Point2D getGlyphPosition(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public void setGlyphPosition(int glyphIndex, Point2D newPos) {
throw new UnsupportedOperationException();
}
@Override
public AffineTransform getGlyphTransform(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public void setGlyphTransform(int glyphIndex, AffineTransform newTX) {
throw new UnsupportedOperationException();
}
@Override
public float[] getGlyphPositions(int beginGlyphIndex, int numEntries, float[] positionReturn) {
throw new UnsupportedOperationException();
}
@Override
public Shape getGlyphLogicalBounds(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public Shape getGlyphVisualBounds(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public GlyphMetrics getGlyphMetrics(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public GlyphJustificationInfo getGlyphJustificationInfo(int glyphIndex) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("CovariantEquals")
@Override
public boolean equals(GlyphVector set) {
throw new UnsupportedOperationException();
}
}
@@ -20,12 +20,8 @@ import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphJustificationInfo;
import java.awt.font.GlyphMetrics;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
public class MockFontLayoutService extends FontLayoutService {
@@ -64,8 +60,8 @@ public class MockFontLayoutService extends FontLayoutService {
public int getDescent(@NotNull FontMetrics fontMetrics) {
return myDescent;
}
private class MockGlyphVector extends GlyphVector {
private class MockGlyphVector extends AbstractMockGlyphVector {
private final char[] myChars;
private final boolean myIsRtl;
@@ -89,100 +85,9 @@ public class MockFontLayoutService extends FontLayoutService {
return new Point(glyphIndex * myCharWidth, 0);
}
@Override
public Font getFont() {
throw new UnsupportedOperationException();
}
@Override
public FontRenderContext getFontRenderContext() {
throw new UnsupportedOperationException();
}
@Override
public void performDefaultLayout() {
throw new UnsupportedOperationException();
}
@Override
public int getGlyphCode(int glyphIndex) {
return myChars[glyphIndex];
}
@Override
public int[] getGlyphCodes(int beginGlyphIndex, int numEntries, int[] codeReturn) {
throw new UnsupportedOperationException();
}
@Override
public Rectangle2D getLogicalBounds() {
throw new UnsupportedOperationException();
}
@Override
public Rectangle2D getVisualBounds() {
throw new UnsupportedOperationException();
}
@Override
public Shape getOutline() {
throw new UnsupportedOperationException();
}
@Override
public Shape getOutline(float x, float y) {
throw new UnsupportedOperationException();
}
@Override
public Shape getGlyphOutline(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public void setGlyphPosition(int glyphIndex, Point2D newPos) {
throw new UnsupportedOperationException();
}
@Override
public AffineTransform getGlyphTransform(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public void setGlyphTransform(int glyphIndex, AffineTransform newTX) {
throw new UnsupportedOperationException();
}
@Override
public float[] getGlyphPositions(int beginGlyphIndex, int numEntries, float[] positionReturn) {
throw new UnsupportedOperationException();
}
@Override
public Shape getGlyphLogicalBounds(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public Shape getGlyphVisualBounds(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public GlyphMetrics getGlyphMetrics(int glyphIndex) {
throw new UnsupportedOperationException();
}
@Override
public GlyphJustificationInfo getGlyphJustificationInfo(int glyphIndex) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("CovariantEquals")
@Override
public boolean equals(GlyphVector set) {
throw new UnsupportedOperationException();
}
}
}