/* * Copyright 2000-2016 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.openapi.diagnostic.Logger; import com.intellij.openapi.editor.impl.FontInfo; import com.intellij.util.BitUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.awt.*; import java.awt.font.GlyphVector; import java.awt.geom.Rectangle2D; import java.util.Arrays; /** * GlyphVector-based text fragment. Used for non-Latin text or when ligatures are enabled */ class ComplexTextFragment extends TextFragment { private static final Logger LOG = Logger.getInstance(ComplexTextFragment.class); private static final double CLIP_MARGIN = 1e4; @NotNull private final GlyphVector myGlyphVector; @Nullable private final short[] myCodePoint2Offset; // Start offset of each Unicode code point in the fragment // (null if each code point takes one char). // We expect no more than 1025 chars in a fragment, so 'short' should be enough. ComplexTextFragment(@NotNull char[] lineChars, int start, int end, boolean isRtl, @NotNull FontInfo fontInfo) { super(end - start); assert start >= 0; assert end <= lineChars.length; assert start < end; myGlyphVector = FontLayoutService.getInstance().layoutGlyphVector(fontInfo.getFont(), fontInfo.getFontRenderContext(), lineChars, start, end, isRtl); int numChars = end - start; int numGlyphs = myGlyphVector.getNumGlyphs(); float totalWidth = (float)myGlyphVector.getGlyphPosition(numGlyphs).getX(); myCharPositions[numChars - 1] = totalWidth; int lastCharIndex = -1; 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 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()) { 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); } } float newX = isRtl ? Math.min(lastX, (float)bounds.getMinX()) : Math.max(lastX, (float)bounds.getMaxX()); newX = Math.max(0, Math.min(totalWidth, newX)); setCharPosition(charIndex, newX, isRtl, numChars); prevX = lastX; lastX = newX; 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); } } int codePointCount = Character.codePointCount(lineChars, start, end - start); if (codePointCount == numChars) { myCodePoint2Offset = null; } else { myCodePoint2Offset = new short[codePointCount]; int offset = 0; for (int i = 0; i < codePointCount; i++) { myCodePoint2Offset[i] = (short)(offset++); if (offset < numChars && Character.isHighSurrogate(lineChars[start + offset - 1]) && Character.isLowSurrogate(lineChars[start + offset])) { offset++; } } } } 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; } } boolean isRtl() { return BitUtil.isSet(myGlyphVector.getLayoutFlags(), GlyphVector.FLAG_RUN_RTL); } @Override int offsetToLogicalColumn(int offset) { if (myCodePoint2Offset == null) return offset; if (offset == getLength()) return myCodePoint2Offset.length; int i = Arrays.binarySearch(myCodePoint2Offset, (short)offset); assert i >= 0; return i; } // Drawing a portion of glyph vector using clipping might be not very effective // (we still pass all glyphs to the rendering code, and filtering by clipping might occur late in the processing, // on OS X larger number of glyphs passed for processing is known to slow down rendering significantly). // So we try to merge drawing of adjacent glyph vector fragments, if possible. private static ComplexTextFragment lastFragment; private static int lastStartColumn; private static int lastEndColumn; private static Color lastColor; private static float lastStartX; private static float lastEndX; private static float lastY; @SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod") @Override public void draw(Graphics2D g, float x, float y, int startColumn, int endColumn) { assert startColumn >= 0; assert endColumn <= myCharPositions.length; assert startColumn < endColumn; Color color = g.getColor(); assert color != null; float newX = x - getX(startColumn) + getX(endColumn); if (lastFragment == this && lastEndColumn == startColumn && lastEndX == x && lastY == y && color.equals(lastColor)) { lastEndColumn = endColumn; lastEndX = newX; return; } flushDrawingCache(g); lastFragment = this; lastStartColumn = startColumn; lastEndColumn = endColumn; lastColor = color; lastStartX = x; lastEndX = newX; lastY = y; } private void doDraw(Graphics2D g, float x, float y, int startColumn, int endColumn) { updateStats(endColumn - startColumn, myCharPositions.length); if (startColumn == 0 && endColumn == myCharPositions.length) { g.drawGlyphVector(myGlyphVector, x, y); } else { Shape savedClip = g.getClip(); float startX = x - getX(startColumn); // We define clip region here assuming that glyphs do not extend further than CLIP_MARGIN pixels from baseline // vertically (both up and down) and horizontally (from the region defined by glyph vector's total advance) double xMin = x - (startColumn == 0 ? CLIP_MARGIN : 0); double xMax = startX + getX(endColumn) + (endColumn == myCharPositions.length ? CLIP_MARGIN : 0); double yMin = y - CLIP_MARGIN; double yMax = y + CLIP_MARGIN; g.clip(new Rectangle2D.Double(xMin, yMin, xMax - xMin, yMax - yMin)); g.drawGlyphVector(myGlyphVector, startX, y); g.setClip(savedClip); } } private int getCodePointCount() { return myCodePoint2Offset == null ? myCharPositions.length : myCodePoint2Offset.length; } private int visualColumnToVisualOffset(int column) { if (myCodePoint2Offset == null) return column; if (column <= 0) return 0; if (column >= myCodePoint2Offset.length) return getLength(); return isRtl() ? getLength() - myCodePoint2Offset[myCodePoint2Offset.length - column] : myCodePoint2Offset[column]; } @Override public int getLogicalColumnCount(int startColumn) { return getCodePointCount(); } @Override public int getVisualColumnCount(float startX) { return getCodePointCount(); } @Override public int[] xToVisualColumn(float startX, float x) { float relX = x - startX; float prevPos = 0; int columnCount = getCodePointCount(); for (int i = 0; i < columnCount; i++) { int visualOffset = visualColumnToVisualOffset(i); float newPos = myCharPositions[visualOffset]; if (relX < (newPos + prevPos) / 2) { return new int[] {i, relX <= prevPos ? 0 : 1}; } prevPos = newPos; } return new int[] {columnCount, relX <= myCharPositions[myCharPositions.length - 1] ? 0 : 1}; } @Override public float visualColumnToX(float startX, int column) { return startX + getX(visualColumnToVisualOffset(column)); } public static void flushDrawingCache(Graphics2D g) { if (lastFragment != null) { g.setColor(lastColor); lastFragment.doDraw(g, lastStartX, lastY, lastStartColumn, lastEndColumn); lastFragment = null; } } private static long ourDrawingCount; private static long ourCharsProcessed; private static long ourGlyphsProcessed; private static void updateStats(int charCount, int glyphCount) { if (!LOG.isDebugEnabled()) return; ourCharsProcessed += charCount; ourGlyphsProcessed += glyphCount; if (++ourDrawingCount == 10000) { LOG.debug("Text rendering stats: " + ourCharsProcessed + " chars, " + ourGlyphsProcessed + " glyps, ratio - " + ((float) ourGlyphsProcessed) / ourCharsProcessed); ourDrawingCount = 0; ourCharsProcessed = 0; ourGlyphsProcessed = 0; } } }