diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ComplexTextFragment.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ComplexTextFragment.java index b2f1b81e7303..5900c9ae0a0e 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ComplexTextFragment.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ComplexTextFragment.java @@ -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) { diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/view/ComplexTextFragmentTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/view/ComplexTextFragmentTest.java new file mode 100644 index 000000000000..7c79fd2cb930 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/view/ComplexTextFragmentTest.java @@ -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; + } + } +} diff --git a/platform/testFramework/src/com/intellij/testFramework/AbstractMockGlyphVector.java b/platform/testFramework/src/com/intellij/testFramework/AbstractMockGlyphVector.java new file mode 100644 index 000000000000..a291c8953313 --- /dev/null +++ b/platform/testFramework/src/com/intellij/testFramework/AbstractMockGlyphVector.java @@ -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(); + } +} diff --git a/platform/testFramework/src/com/intellij/testFramework/MockFontLayoutService.java b/platform/testFramework/src/com/intellij/testFramework/MockFontLayoutService.java index c9349504336f..faa550aa8824 100644 --- a/platform/testFramework/src/com/intellij/testFramework/MockFontLayoutService.java +++ b/platform/testFramework/src/com/intellij/testFramework/MockFontLayoutService.java @@ -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(); - } } }