mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
caret positioning inside ligatures
This commit is contained in:
+44
-9
@@ -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) {
|
||||
|
||||
+142
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user