/*
* Copyright 2000-2017 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.editor.*;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorFontType;
import com.intellij.openapi.editor.ex.MarkupModelEx;
import com.intellij.openapi.editor.ex.RangeHighlighterEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.editor.impl.*;
import com.intellij.openapi.editor.impl.softwrap.SoftWrapDrawingType;
import com.intellij.openapi.editor.markup.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.impl.IdeBackgroundUtil;
import com.intellij.ui.ColorUtil;
import com.intellij.ui.Gray;
import com.intellij.ui.JBColor;
import com.intellij.ui.paint.EffectPainter;
import com.intellij.util.DocumentUtil;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import gnu.trove.TFloatArrayList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.GeneralPath;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Renders editor contents.
*/
class EditorPainter implements TextDrawingCallback {
private static final Color CARET_LIGHT = Gray._255;
private static final Color CARET_DARK = Gray._0;
private static final Stroke IME_COMPOSED_TEXT_UNDERLINE_STROKE = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0,
new float[]{0, 2, 0, 2}, 0);
private static final int CARET_DIRECTION_MARK_SIZE = 5;
private static final char IDEOGRAPHIC_SPACE = '\u3000'; // http://www.marathon-studios.com/unicode/U3000/Ideographic_Space
private static final String WHITESPACE_CHARS = " \t" + IDEOGRAPHIC_SPACE;
private final EditorView myView;
private final EditorImpl myEditor;
private final Document myDocument;
EditorPainter(EditorView view) {
myView = view;
myEditor = view.getEditor();
myDocument = myEditor.getDocument();
}
void paint(Graphics2D g) {
Rectangle clip = g.getClipBounds();
if (myEditor.getContentComponent().isOpaque()) {
g.setColor(myEditor.getBackgroundColor());
g.fillRect(clip.x, clip.y, clip.width, clip.height);
}
if (paintPlaceholderText(g)) {
paintCaret(g);
return;
}
int startLine = myView.yToVisualLine(clip.y);
int endLine = myView.yToVisualLine(clip.y + clip.height);
int startOffset = myView.visualLineToOffset(startLine);
int endOffset = myView.visualLineToOffset(endLine + 1);
ClipDetector clipDetector = new ClipDetector(myEditor, clip);
IterationState.CaretData caretData = myEditor.isPaintSelection() ? IterationState.createCaretData(myEditor) : null;
paintBackground(g, clip, startLine, endLine, caretData);
paintRightMargin(g, clip);
paintCustomRenderers(g, startOffset, endOffset);
MarkupModelEx docMarkup = myEditor.getFilteredDocumentMarkupModel();
paintLineMarkersSeparators(g, clip, docMarkup, startOffset, endOffset);
paintLineMarkersSeparators(g, clip, myEditor.getMarkupModel(), startOffset, endOffset);
paintTextWithEffects(g, clip, startLine, endLine, caretData);
paintHighlightersAfterEndOfLine(g, docMarkup, startOffset, endOffset);
paintHighlightersAfterEndOfLine(g, myEditor.getMarkupModel(), startOffset, endOffset);
paintBorderEffect(g, clipDetector, myEditor.getHighlighter(), startOffset, endOffset);
paintBorderEffect(g, clipDetector, docMarkup, startOffset, endOffset);
paintBorderEffect(g, clipDetector, myEditor.getMarkupModel(), startOffset, endOffset);
paintCaret(g);
paintComposedTextDecoration(g);
}
private boolean paintPlaceholderText(Graphics2D g) {
CharSequence hintText = myEditor.getPlaceholder();
EditorComponentImpl editorComponent = myEditor.getContentComponent();
if (myDocument.getTextLength() > 0 || hintText == null || hintText.length() == 0 ||
KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() == editorComponent &&
!myEditor.getShowPlaceholderWhenFocused()) {
return false;
}
hintText = SwingUtilities.layoutCompoundLabel(g.getFontMetrics(), hintText.toString(), null, 0, 0, 0, 0,
SwingUtilities.calculateInnerArea(editorComponent, null), // account for insets
new Rectangle(), new Rectangle(), 0);
EditorFontType fontType = EditorFontType.PLAIN;
Color color = myEditor.getFoldingModel().getPlaceholderAttributes().getForegroundColor();
TextAttributes attributes = myEditor.getPlaceholderAttributes();
if (attributes != null) {
int type = attributes.getFontType();
if (type == Font.ITALIC) fontType = EditorFontType.ITALIC;
else if (type == Font.BOLD) fontType = EditorFontType.BOLD;
else if (type == (Font.ITALIC | Font.BOLD)) fontType = EditorFontType.BOLD_ITALIC;
Color attColor = attributes.getForegroundColor();
if (attColor != null) color = attColor;
}
g.setColor(color);
g.setFont(myEditor.getColorsScheme().getFont(fontType));
Insets insets = myView.getInsets();
g.drawString(hintText.toString(), insets.left, insets.top + myView.getAscent());
return true;
}
private void paintRightMargin(Graphics g, Rectangle clip) {
if (!isRightMarginShown()) return;
int x = getRightMarginX();
g.setColor(myEditor.getColorsScheme().getColor(EditorColors.RIGHT_MARGIN_COLOR));
UIUtil.drawLine(g, x, clip.y, x, clip.y + clip.height);
}
private boolean isRightMarginShown() {
return myEditor.getSettings().isRightMarginShown() && myEditor.getColorsScheme().getColor(EditorColors.RIGHT_MARGIN_COLOR) != null;
}
private int getRightMarginX() {
return getMinX() + myEditor.getSettings().getRightMargin(myEditor.getProject()) * myView.getPlainSpaceWidth();
}
private int getMinX() {
return myView.getInsets().left;
}
private void paintBackground(Graphics2D g, Rectangle clip, int startVisualLine, int endVisualLine, IterationState.CaretData caretData) {
int lineCount = myEditor.getVisibleLineCount();
final Map<Integer, Couple<Integer>> virtualSelectionMap = createVirtualSelectionMap(startVisualLine, endVisualLine);
final VisualPosition primarySelectionStart = myEditor.getSelectionModel().getSelectionStartPosition();
final VisualPosition primarySelectionEnd = myEditor.getSelectionModel().getSelectionEndPosition();
LineLayout prefixLayout = myView.getPrefixLayout();
if (startVisualLine == 0 && prefixLayout != null) {
final Insets insets = myView.getInsets();
paintBackground(g, myView.getPrefixAttributes(), insets.left, insets.top, prefixLayout.getWidth());
}
VisualLinesIterator visLinesIterator = new VisualLinesIterator(myEditor, startVisualLine);
while (!visLinesIterator.atEnd()) {
int visualLine = visLinesIterator.getVisualLine();
if (visualLine > endVisualLine || visualLine >= lineCount) break;
int y = visLinesIterator.getY();
paintLineFragments(g, clip, visLinesIterator, caretData, y, new LineFragmentPainter() {
@Override
public void paintBeforeLineStart(Graphics2D g, TextAttributes attributes, int columnEnd, float xEnd, int y) {
paintBackground(g, attributes, getMinX(), y, xEnd);
paintSelectionOnSecondSoftWrapLineIfNecessary(g, visualLine, columnEnd, xEnd, y, primarySelectionStart, primarySelectionEnd);
}
@Override
public void paint(Graphics2D g, VisualLineFragmentsIterator.Fragment fragment, int start, int end,
TextAttributes attributes, float xStart, float xEnd, int y) {
paintBackground(g, attributes, xStart, y, xEnd - xStart);
}
@Override
public void paintAfterLineEnd(Graphics2D g, Rectangle clip, IterationState it, int columnStart, float x, int y) {
paintBackground(g, it.getPastLineEndBackgroundAttributes(), x, y, clip.x + clip.width - x);
int offset = it.getEndOffset();
SoftWrap softWrap = myEditor.getSoftWrapModel().getSoftWrap(offset);
if (softWrap == null) {
paintVirtualSelectionIfNecessary(g, visualLine, virtualSelectionMap, columnStart, x, clip.x + clip.width, y);
}
else {
paintSelectionOnFirstSoftWrapLineIfNecessary(g, visualLine, columnStart, x, clip.x + clip.width, y,
primarySelectionStart, primarySelectionEnd);
}
}
});
visLinesIterator.advance();
}
}
private Map<Integer, Couple<Integer>> createVirtualSelectionMap(int startVisualLine, int endVisualLine) {
HashMap<Integer, Couple<Integer>> map = new HashMap<>();
for (Caret caret : myEditor.getCaretModel().getAllCarets()) {
if (caret.hasSelection()) {
VisualPosition selectionStart = caret.getSelectionStartPosition();
VisualPosition selectionEnd = caret.getSelectionEndPosition();
if (selectionStart.line == selectionEnd.line) {
int line = selectionStart.line;
if (line >= startVisualLine && line <= endVisualLine) {
map.put(line, Couple.of(selectionStart.column, selectionEnd.column));
}
}
}
}
return map;
}
private void paintVirtualSelectionIfNecessary(Graphics2D g,
int visualLine,
Map<Integer, Couple<Integer>> virtualSelectionMap,
int columnStart,
float xStart,
float xEnd,
int y) {
Couple<Integer> selectionRange = virtualSelectionMap.get(visualLine);
if (selectionRange == null || selectionRange.second <= columnStart) return;
float startX = selectionRange.first <= columnStart ? xStart :
(float)myView.visualPositionToXY(new VisualPosition(visualLine, selectionRange.first)).getX();
float endX = (float)Math.min(xEnd, myView.visualPositionToXY(new VisualPosition(visualLine, selectionRange.second)).getX());
paintBackground(g, myEditor.getColorsScheme().getColor(EditorColors.SELECTION_BACKGROUND_COLOR), startX, y, endX - startX);
}
private void paintSelectionOnSecondSoftWrapLineIfNecessary(Graphics2D g, int visualLine, int columnEnd, float xEnd, int y,
VisualPosition selectionStartPosition, VisualPosition selectionEndPosition) {
if (selectionStartPosition.equals(selectionEndPosition) ||
visualLine < selectionStartPosition.line ||
visualLine > selectionEndPosition.line ||
visualLine == selectionStartPosition.line && selectionStartPosition.column >= columnEnd) {
return;
}
float startX = (selectionStartPosition.line == visualLine && selectionStartPosition.column > 0) ?
(float)myView.visualPositionToXY(selectionStartPosition).getX() : getMinX();
float endX = (selectionEndPosition.line == visualLine && selectionEndPosition.column < columnEnd) ?
(float)myView.visualPositionToXY(selectionEndPosition).getX() : xEnd;
paintBackground(g, myEditor.getColorsScheme().getColor(EditorColors.SELECTION_BACKGROUND_COLOR), startX, y, endX - startX);
}
private void paintSelectionOnFirstSoftWrapLineIfNecessary(Graphics2D g, int visualLine, int columnStart, float xStart, float xEnd, int y,
VisualPosition selectionStartPosition, VisualPosition selectionEndPosition) {
if (selectionStartPosition.equals(selectionEndPosition) ||
visualLine < selectionStartPosition.line ||
visualLine > selectionEndPosition.line ||
visualLine == selectionEndPosition.line && selectionEndPosition.column <= columnStart) {
return;
}
float startX = selectionStartPosition.line == visualLine && selectionStartPosition.column > columnStart ?
(float)myView.visualPositionToXY(selectionStartPosition).getX() : xStart;
float endX = selectionEndPosition.line == visualLine ?
(float)myView.visualPositionToXY(selectionEndPosition).getX() : xEnd;
paintBackground(g, myEditor.getColorsScheme().getColor(EditorColors.SELECTION_BACKGROUND_COLOR), startX, y, endX - startX);
}
private void paintBackground(Graphics2D g, TextAttributes attributes, float x, int y, float width) {
if (attributes == null) return;
paintBackground(g, attributes.getBackgroundColor(), x, y, width);
}
private void paintBackground(Graphics2D g, Color color, float x, int y, float width) {
if (width <= 0 ||
color == null ||
color.equals(myEditor.getColorsScheme().getDefaultBackground()) ||
color.equals(myEditor.getBackgroundColor())) return;
g.setColor(color);
int xStartRounded = (int)x;
int xEndRounded = (int)(x + width);
g.fillRect(xStartRounded, y, xEndRounded - xStartRounded, myView.getLineHeight());
}
private void paintCustomRenderers(final Graphics2D g, final int startOffset, final int endOffset) {
myEditor.getMarkupModel().processRangeHighlightersOverlappingWith(startOffset, endOffset, highlighter -> {
CustomHighlighterRenderer customRenderer = highlighter.getCustomRenderer();
if (customRenderer != null && startOffset < highlighter.getEndOffset() && highlighter.getStartOffset() < endOffset) {
customRenderer.paint(myEditor, highlighter, g);
}
return true;
});
}
private void paintLineMarkersSeparators(final Graphics g,
final Rectangle clip,
MarkupModelEx markupModel,
int startOffset,
int endOffset) {
markupModel.processRangeHighlightersOverlappingWith(startOffset, endOffset, highlighter -> {
paintLineMarkerSeparator(highlighter, clip, g);
return true;
});
}
private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) {
Color separatorColor = marker.getLineSeparatorColor();
LineSeparatorRenderer lineSeparatorRenderer = marker.getLineSeparatorRenderer();
if (separatorColor == null && lineSeparatorRenderer == null) {
return;
}
int line = myDocument.getLineNumber(marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP
? marker.getStartOffset()
: marker.getEndOffset());
int visualLine = myView.logicalToVisualPosition(new LogicalPosition(line + (marker.getLineSeparatorPlacement() ==
SeparatorPlacement.TOP ? 0 : 1), 0), false).line;
int y = myView.visualLineToY(visualLine) - 1;
int startX = getMinX();
int endX = clip.x + clip.width;
if (isRightMarginShown()) {
endX = Math.min(endX, getRightMarginX());
}
g.setColor(separatorColor);
if (lineSeparatorRenderer != null) {
lineSeparatorRenderer.drawLine(g, startX, endX, y);
}
else {
UIUtil.drawLine(g, startX, y, endX, y);
}
}
private void paintTextWithEffects(Graphics2D g, Rectangle clip, int startVisualLine, int endVisualLine,
IterationState.CaretData caretData) {
final CharSequence text = myDocument.getImmutableCharSequence();
final LineWhitespacePaintingStrategy whitespacePaintingStrategy = new LineWhitespacePaintingStrategy(myEditor.getSettings());
boolean paintAllSoftWraps = myEditor.getSettings().isAllSoftWrapsShown();
int lineCount = myEditor.getVisibleLineCount();
final int whiteSpaceStrokeWidth = JBUI.scale(1);
final Stroke whiteSpaceStroke = new BasicStroke(whiteSpaceStrokeWidth);
LineLayout prefixLayout = myView.getPrefixLayout();
if (startVisualLine == 0 && prefixLayout != null) {
g.setColor(myView.getPrefixAttributes().getForegroundColor());
paintLineLayoutWithEffect(g, prefixLayout, getMinX(), myView.getAscent(),
myView.getPrefixAttributes().getEffectColor(), myView.getPrefixAttributes().getEffectType());
}
VisualLinesIterator visLinesIterator = new VisualLinesIterator(myEditor, startVisualLine);
while (!visLinesIterator.atEnd()) {
int visualLine = visLinesIterator.getVisualLine();
if (visualLine > endVisualLine || visualLine >= lineCount) break;
int y = visLinesIterator.getY();
final boolean paintSoftWraps = paintAllSoftWraps ||
myEditor.getCaretModel().getLogicalPosition().line == visLinesIterator.getStartLogicalLine();
final int[] currentLogicalLine = new int[] {-1};
paintLineFragments(g, clip, visLinesIterator, caretData, y + myView.getAscent(), new LineFragmentPainter() {
@Override
public void paintBeforeLineStart(Graphics2D g, TextAttributes attributes, int columnEnd, float xEnd, int y) {
if (paintSoftWraps) {
SoftWrapModelImpl softWrapModel = myEditor.getSoftWrapModel();
int symbolWidth = softWrapModel.getMinDrawingWidthInPixels(SoftWrapDrawingType.AFTER_SOFT_WRAP);
softWrapModel.doPaint(g, SoftWrapDrawingType.AFTER_SOFT_WRAP,
(int)xEnd - symbolWidth, y - myView.getAscent(), myView.getLineHeight());
}
}
@Override
public void paint(Graphics2D g, VisualLineFragmentsIterator.Fragment fragment, int start, int end,
TextAttributes attributes, float xStart, float xEnd, int y) {
int lineHeight = myView.getLineHeight();
List<Inlay> inlays = fragment.getCurrentInlays();
if (inlays != null) {
for (Inlay inlay : inlays) {
EditorCustomElementRenderer renderer = inlay.getRenderer();
int width = inlay.getWidthInPixels();
renderer.paint(myEditor, g, new Rectangle((int) xStart, y - myView.getAscent(), width, lineHeight));
xStart += width;
}
return;
}
boolean allowBorder = fragment.getCurrentFoldRegion() != null;
if (attributes != null && hasTextEffect(attributes.getEffectColor(), attributes.getEffectType(), allowBorder)) {
paintTextEffect(g, xStart, xEnd, y, attributes.getEffectColor(), attributes.getEffectType(), allowBorder);
}
if (attributes != null && attributes.getForegroundColor() != null) {
g.setColor(attributes.getForegroundColor());
fragment.draw(g, xStart, y, start, end);
}
if (fragment.getCurrentFoldRegion() == null) {
int logicalLine = fragment.getStartLogicalLine();
if (logicalLine != currentLogicalLine[0]) {
whitespacePaintingStrategy.update(text, myDocument.getLineStartOffset(logicalLine), myDocument.getLineEndOffset(logicalLine));
currentLogicalLine[0] = logicalLine;
}
paintWhitespace(g, text, xStart, y, start, end, whitespacePaintingStrategy, fragment, whiteSpaceStroke, whiteSpaceStrokeWidth);
}
}
@Override
public void paintAfterLineEnd(Graphics2D g, Rectangle clip, IterationState iterationState, int columnStart, float x, int y) {
int offset = iterationState.getEndOffset();
SoftWrapModelImpl softWrapModel = myEditor.getSoftWrapModel();
if (softWrapModel.getSoftWrap(offset) == null) {
int logicalLine = myDocument.getLineNumber(offset);
paintLineExtensions(g, logicalLine, x, y);
}
else if (paintSoftWraps) {
softWrapModel.doPaint(g, SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED,
(int)x, y - myView.getAscent(), myView.getLineHeight());
}
}
});
visLinesIterator.advance();
}
ComplexTextFragment.flushDrawingCache(g);
}
private float paintLineLayoutWithEffect(Graphics2D g, LineLayout layout, float x, float y,
@Nullable Color effectColor, @Nullable EffectType effectType) {
if (hasTextEffect(effectColor, effectType, false)) {
paintTextEffect(g, x, x + layout.getWidth(), (int)y, effectColor, effectType, false);
}
for (LineLayout.VisualFragment fragment : layout.getFragmentsInVisualOrder(x)) {
fragment.draw(g, x, y);
x = fragment.getEndX();
}
return x;
}
private static boolean hasTextEffect(@Nullable Color effectColor, @Nullable EffectType effectType, boolean allowBorder) {
return effectColor != null && (effectType == EffectType.LINE_UNDERSCORE ||
effectType == EffectType.BOLD_LINE_UNDERSCORE ||
effectType == EffectType.BOLD_DOTTED_LINE ||
effectType == EffectType.WAVE_UNDERSCORE ||
effectType == EffectType.STRIKEOUT ||
allowBorder && (effectType == EffectType.BOXED || effectType == EffectType.ROUNDED_BOX));
}
private void paintTextEffect(Graphics2D g, float xFrom, float xTo, int y, Color effectColor, EffectType effectType, boolean allowBorder) {
g.setColor(effectColor);
int xStart = (int)xFrom;
int xEnd = (int)xTo;
if (effectType == EffectType.LINE_UNDERSCORE) {
EffectPainter.LINE_UNDERSCORE.paint(g, xStart, y, xEnd - xStart, myView.getDescent(),
myEditor.getColorsScheme().getFont(EditorFontType.PLAIN));
}
else if (effectType == EffectType.BOLD_LINE_UNDERSCORE) {
EffectPainter.BOLD_LINE_UNDERSCORE.paint(g, xStart, y, xEnd - xStart, myView.getDescent(),
myEditor.getColorsScheme().getFont(EditorFontType.PLAIN));
}
else if (effectType == EffectType.STRIKEOUT) {
EffectPainter.STRIKE_THROUGH.paint(g, xStart, y, xEnd - xStart, myView.getCharHeight(),
myEditor.getColorsScheme().getFont(EditorFontType.PLAIN));
}
else if (effectType == EffectType.WAVE_UNDERSCORE) {
EffectPainter.WAVE_UNDERSCORE.paint(g, xStart, y, xEnd - xStart, myView.getDescent(),
myEditor.getColorsScheme().getFont(EditorFontType.PLAIN));
}
else if (effectType == EffectType.BOLD_DOTTED_LINE) {
EffectPainter.BOLD_DOTTED_UNDERSCORE.paint(g, xStart, y, xEnd - xStart, myView.getDescent(),
myEditor.getColorsScheme().getFont(EditorFontType.PLAIN));
}
else if (allowBorder && (effectType == EffectType.BOXED || effectType == EffectType.ROUNDED_BOX)) {
drawSimpleBorder(g, xFrom, xTo, y - myView.getAscent(), effectType == EffectType.ROUNDED_BOX);
}
}
private void paintWhitespace(Graphics2D g, CharSequence text, float x, int y, int start, int end,
LineWhitespacePaintingStrategy whitespacePaintingStrategy,
VisualLineFragmentsIterator.Fragment fragment, Stroke stroke, int strokeWidth) {
Stroke oldStroke = g.getStroke();
try {
g.setColor(myEditor.getColorsScheme().getColor(EditorColors.WHITESPACES_COLOR));
g.setStroke(stroke); // applied for tab & ideographic space
boolean isRtl = fragment.isRtl();
int baseStartOffset = fragment.getStartOffset();
int startOffset = isRtl ? baseStartOffset - start : baseStartOffset + start;
y -= 1;
for (int i = start; i < end; i++) {
int charOffset = isRtl ? baseStartOffset - i - 1 : baseStartOffset + i;
char c = text.charAt(charOffset);
if (" \t\u3000".indexOf(c) >= 0 && whitespacePaintingStrategy.showWhitespaceAtOffset(charOffset)) {
int startX = (int)fragment.offsetToX(x, startOffset, isRtl ? baseStartOffset - i : baseStartOffset + i);
int endX = (int)fragment.offsetToX(x, startOffset, isRtl ? baseStartOffset - i - 1 : baseStartOffset + i + 1);
if (c == ' ') {
//noinspection SuspiciousNameCombination
g.fillRect((startX + endX - strokeWidth) / 2, y - strokeWidth + 1, strokeWidth, strokeWidth);
}
else if (c == '\t') {
endX -= myView.getPlainSpaceWidth() / 4;
int height = myView.getCharHeight();
int halfHeight = height / 2;
int mid = y - halfHeight;
int top = y - height;
UIUtil.drawLine(g, startX, mid, endX, mid);
UIUtil.drawLine(g, endX, y, endX, top);
g.fillPolygon(new int[]{endX - halfHeight, endX - halfHeight, endX}, new int[]{y, y - height, y - halfHeight}, 3);
}
else if (c == '\u3000') { // ideographic space
int charHeight = myView.getCharHeight();
g.drawRect(startX + JBUI.scale(2) + strokeWidth/2, y - charHeight + strokeWidth/2,
endX - startX - JBUI.scale(4) - (strokeWidth - 1), charHeight - (strokeWidth - 1));
}
}
}
} finally {
g.setStroke(oldStroke);
}
}
private void paintLineExtensions(Graphics2D g, int line, float x, int y) {
Project project = myEditor.getProject();
VirtualFile virtualFile = myEditor.getVirtualFile();
if (project == null || virtualFile == null) return;
for (EditorLinePainter painter : EditorLinePainter.EP_NAME.getExtensions()) {
Collection<LineExtensionInfo> extensions = painter.getLineExtensions(project, virtualFile, line);
if (extensions != null) {
for (LineExtensionInfo info : extensions) {
LineLayout layout = LineLayout.create(myView, info.getText(), info.getFontType());
g.setColor(info.getColor());
x = paintLineLayoutWithEffect(g, layout, x, y, info.getEffectColor(), info.getEffectType());
int currentLineWidth = (int)x - getMinX();
EditorSizeManager sizeManager = myView.getSizeManager();
if (currentLineWidth > sizeManager.getMaxLineWithExtensionWidth()) {
sizeManager.setMaxLineWithExtensionWidth(line, currentLineWidth);
}
}
}
}
}
private void paintHighlightersAfterEndOfLine(final Graphics2D g,
MarkupModelEx markupModel,
final int startOffset,
int endOffset) {
markupModel.processRangeHighlightersOverlappingWith(startOffset, endOffset, highlighter -> {
if (highlighter.getStartOffset() >= startOffset) {
paintHighlighterAfterEndOfLine(g, highlighter);
}
return true;
});
}
private void paintHighlighterAfterEndOfLine(Graphics2D g, RangeHighlighterEx highlighter) {
if (!highlighter.isAfterEndOfLine()) {
return;
}
int startOffset = highlighter.getStartOffset();
int lineEndOffset = myDocument.getLineEndOffset(myDocument.getLineNumber(startOffset));
if (myEditor.getFoldingModel().isOffsetCollapsed(lineEndOffset)) return;
Point2D lineEnd = myView.offsetToXY(lineEndOffset, true, false);
float x = (float)lineEnd.getX();
int y = (int)lineEnd.getY();
TextAttributes attributes = highlighter.getTextAttributes();
paintBackground(g, attributes, x, y, myView.getPlainSpaceWidth());
if (attributes != null && hasTextEffect(attributes.getEffectColor(), attributes.getEffectType(), false)) {
paintTextEffect(g, x, x + myView.getPlainSpaceWidth() - 1, y + myView.getAscent(),
attributes.getEffectColor(), attributes.getEffectType(), false);
}
}
private void paintBorderEffect(Graphics2D g,
ClipDetector clipDetector,
EditorHighlighter highlighter,
int clipStartOffset,
int clipEndOffset) {
HighlighterIterator it = highlighter.createIterator(clipStartOffset);
while (!it.atEnd() && it.getStart() < clipEndOffset) {
TextAttributes attributes = it.getTextAttributes();
if (isBorder(attributes)) {
paintBorderEffect(g, clipDetector, it.getStart(), it.getEnd(), attributes);
}
it.advance();
}
}
private void paintBorderEffect(final Graphics2D g,
final ClipDetector clipDetector,
MarkupModelEx markupModel,
int clipStartOffset,
int clipEndOffset) {
markupModel.processRangeHighlightersOverlappingWith(clipStartOffset, clipEndOffset, rangeHighlighter -> {
TextAttributes attributes = rangeHighlighter.getTextAttributes();
if (isBorder(attributes)) {
paintBorderEffect(g, clipDetector, rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset(),
attributes);
}
return true;
});
}
private static boolean isBorder(TextAttributes attributes) {
return attributes != null &&
(attributes.getEffectType() == EffectType.BOXED || attributes.getEffectType() == EffectType.ROUNDED_BOX) &&
attributes.getEffectColor() != null;
}
private void paintBorderEffect(Graphics2D g, ClipDetector clipDetector, int startOffset, int endOffset, TextAttributes attributes) {
startOffset = DocumentUtil.alignToCodePointBoundary(myDocument, startOffset);
endOffset = DocumentUtil.alignToCodePointBoundary(myDocument, endOffset);
if (!clipDetector.rangeCanBeVisible(startOffset, endOffset)) return;
int startLine = myDocument.getLineNumber(startOffset);
int endLine = myDocument.getLineNumber(endOffset);
if (startLine + 1 == endLine &&
startOffset == myDocument.getLineStartOffset(startLine) &&
endOffset == myDocument.getLineStartOffset(endLine)) {
// special case of line highlighters
endLine--;
endOffset = myDocument.getLineEndOffset(endLine);
}
boolean rounded = attributes.getEffectType() == EffectType.ROUNDED_BOX;
g.setColor(attributes.getEffectColor());
VisualPosition startPosition = myView.offsetToVisualPosition(startOffset, true, false);
VisualPosition endPosition = myView.offsetToVisualPosition(endOffset, false, true);
if (startPosition.line == endPosition.line) {
int y = myView.visualLineToY(startPosition.line);
TFloatArrayList ranges = adjustedLogicalRangeToVisualRanges(startOffset, endOffset);
for (int i = 0; i < ranges.size() - 1; i+= 2) {
float startX = ranges.get(i);
float endX = ranges.get(i + 1);
drawSimpleBorder(g, startX, endX + 1, y, rounded);
}
}
else {
TFloatArrayList leadingRanges = adjustedLogicalRangeToVisualRanges(
startOffset, myView.visualPositionToOffset(new VisualPosition(startPosition.line, Integer.MAX_VALUE, true)));
TFloatArrayList trailingRanges = adjustedLogicalRangeToVisualRanges(
myView.visualPositionToOffset(new VisualPosition(endPosition.line, 0)), endOffset);
if (!leadingRanges.isEmpty() && !trailingRanges.isEmpty()) {
int minX = getMinX();
int maxX = Math.max(minX + myView.getMaxWidthInLineRange(startPosition.line, endPosition.line - 1) - 1,
(int)trailingRanges.get(trailingRanges.size() - 1));
boolean containsInnerLines = endPosition.line > startPosition.line + 1;
int lineHeight = myView.getLineHeight() - 1;
int leadingTopY = myView.visualLineToY(startPosition.line);
int leadingBottomY = leadingTopY + lineHeight;
int trailingTopY = myView.visualLineToY(endPosition.line);
int trailingBottomY = trailingTopY + lineHeight;
float start = 0;
float end = 0;
float leftGap = leadingRanges.get(0) - (containsInnerLines ? minX : trailingRanges.get(0));
int adjustY = leftGap == 0 ? 2 : leftGap > 0 ? 1 : 0; // avoiding 1-pixel gap between aligned lines
for (int i = 0; i < leadingRanges.size() - 1; i += 2) {
start = leadingRanges.get(i);
end = leadingRanges.get(i + 1);
if (i > 0) {
drawLine(g, leadingRanges.get(i - 1), leadingBottomY, start, leadingBottomY, rounded);
}
drawLine(g, start, leadingBottomY + (i == 0 ? adjustY : 0), start, leadingTopY, rounded);
if ((i + 2) < leadingRanges.size()) {
drawLine(g, start, leadingTopY, end, leadingTopY, rounded);
drawLine(g, end, leadingTopY, end, leadingBottomY, rounded);
}
}
end = Math.max(end, maxX);
drawLine(g, start, leadingTopY, end, leadingTopY, rounded);
drawLine(g, end, leadingTopY, end, trailingTopY - 1, rounded);
float targetX = trailingRanges.get(trailingRanges.size() - 1);
drawLine(g, end, trailingTopY - 1, targetX, trailingTopY - 1, rounded);
adjustY = end == targetX ? -2 : -1; // for lastX == targetX we need to avoid a gap when rounding is used
for (int i = trailingRanges.size() - 2; i >= 0; i -= 2) {
start = trailingRanges.get(i);
end = trailingRanges.get(i + 1);
drawLine(g, end, trailingTopY + (i == 0 ? adjustY : 0), end, trailingBottomY, rounded);
drawLine(g, end, trailingBottomY, start, trailingBottomY, rounded);
drawLine(g, start, trailingBottomY, start, trailingTopY, rounded);
if (i > 0) {
drawLine(g, start, trailingTopY, trailingRanges.get(i - 1), trailingTopY, rounded);
}
}
float lastX = start;
if (containsInnerLines) {
if (start > minX) {
drawLine(g, start, trailingTopY, start, trailingTopY - 1, rounded);
drawLine(g, start, trailingTopY - 1, minX, trailingTopY - 1, rounded);
drawLine(g, minX, trailingTopY - 1, minX, leadingBottomY + 1, rounded);
}
else {
drawLine(g, minX, trailingTopY, minX, leadingBottomY + 1, rounded);
}
lastX = minX;
}
targetX = leadingRanges.get(0);
if (lastX < targetX) {
drawLine(g, lastX, leadingBottomY + 1, targetX, leadingBottomY + 1, rounded);
}
else {
drawLine(g, lastX, leadingBottomY + 1, lastX, leadingBottomY, rounded);
drawLine(g, lastX, leadingBottomY, targetX, leadingBottomY, rounded);
}
}
}
}
private void drawSimpleBorder(Graphics2D g, float xStart, float xEnd, float y, boolean rounded) {
Shape border = getBorderShape(xStart, y, xEnd - xStart, myView.getLineHeight(), rounded);
if (border != null) {
Object old = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.fill(border);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);
}
}
private static Shape getBorderShape(float x, float y, float width, int height, boolean rounded) {
if (width <= 0 || height <= 0) return null;
Shape outer = rounded
? new RoundRectangle2D.Float(x, y, width, height, 2, 2)
: new Rectangle2D.Float(x, y, width, height);
if (width <= 2 || height <= 2) return outer;
Shape inner = new Rectangle2D.Float(x + 1, y + 1, width - 2, height - 2);
Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD);
path.append(outer, false);
path.append(inner, false);
return path;
}
private static void drawLine(Graphics2D g, float x1, int y1, float x2, int y2, boolean rounded) {
if (rounded) {
UIUtil.drawLinePickedOut(g, (int) x1, y1, (int)x2, y2);
} else {
UIUtil.drawLine(g, (int)x1, y1, (int)x2, y2);
}
}
/**
* Returns ranges obtained from {@link #logicalRangeToVisualRanges(int, int)}, adjusted for painting range border - lines should
* line inside target ranges (except for empty range). Target offsets are supposed to be located on the same visual line.
*/
private TFloatArrayList adjustedLogicalRangeToVisualRanges(int startOffset, int endOffset) {
TFloatArrayList ranges = logicalRangeToVisualRanges(startOffset, endOffset);
for (int i = 0; i < ranges.size() - 1; i += 2) {
float startX = ranges.get(i);
float endX = ranges.get(i + 1);
if (startX == endX) {
if (startX > 0) {
startX--;
}
else {
endX++;
}
}
else {
endX--;
}
ranges.set(i, startX);
ranges.set(i + 1, endX);
}
return ranges;
}
/**
* Returns a list of pairs of x coordinates for visual ranges representing given logical range. If
* <code>startOffset == endOffset</code>, a pair of equal numbers is returned, corresponding to target position. Target offsets are
* supposed to be located on the same visual line.
*/
private TFloatArrayList logicalRangeToVisualRanges(int startOffset, int endOffset) {
assert startOffset <= endOffset;
TFloatArrayList result = new TFloatArrayList();
if (myDocument.getTextLength() == 0) {
int minX = getMinX();
result.add(minX);
result.add(minX);
}
else {
for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, startOffset, false)) {
int minOffset = fragment.getMinOffset();
int maxOffset = fragment.getMaxOffset();
if (startOffset == endOffset) {
if (startOffset >= minOffset && startOffset <= maxOffset) {
float x = fragment.offsetToX(startOffset);
result.add(x);
result.add(x);
break;
}
}
else if (startOffset < maxOffset && endOffset > minOffset) {
float x1 = minOffset == maxOffset ? fragment.getStartX() : fragment.offsetToX(Math.max(minOffset, startOffset));
float x2 = minOffset == maxOffset ? fragment.getEndX() : fragment.offsetToX(Math.min(maxOffset, endOffset));
if (x1 > x2) {
float tmp = x1;
x1 = x2;
x2 = tmp;
}
if (result.isEmpty() || x1 > result.get(result.size() - 1)) {
result.add(x1);
result.add(x2);
}
else {
result.set(result.size() - 1, x2);
}
}
}
}
return result;
}
private void paintComposedTextDecoration(Graphics2D g) {
TextRange composedTextRange = myEditor.getComposedTextRange();
if (composedTextRange != null) {
Point2D p1 = myView.offsetToXY(Math.min(composedTextRange.getStartOffset(), myDocument.getTextLength()), true, false);
Point2D p2 = myView.offsetToXY(Math.min(composedTextRange.getEndOffset(), myDocument.getTextLength()), false, true);
int y = (int)p1.getY() + myView.getAscent() + 1;
g.setStroke(IME_COMPOSED_TEXT_UNDERLINE_STROKE);
g.setColor(myEditor.getColorsScheme().getDefaultForeground());
UIUtil.drawLine(g, (int)p1.getX(), y, (int)p2.getX(), y);
}
}
private void paintCaret(Graphics2D g_) {
EditorImpl.CaretRectangle[] locations = myEditor.getCaretLocations(true);
if (locations == null) return;
Graphics2D g = IdeBackgroundUtil.getOriginalGraphics(g_);
int nominalLineHeight = myView.getNominalLineHeight();
int topOverhang = myView.getTopOverhang();
EditorSettings settings = myEditor.getSettings();
Color caretColor = myEditor.getColorsScheme().getColor(EditorColors.CARET_COLOR);
if (caretColor == null) caretColor = new JBColor(CARET_DARK, CARET_LIGHT);
int minX = getMinX();
for (EditorImpl.CaretRectangle location : locations) {
float x = location.myPoint.x;
int y = location.myPoint.y - topOverhang;
Caret caret = location.myCaret;
CaretVisualAttributes attr = caret == null ? CaretVisualAttributes.DEFAULT : caret.getVisualAttributes();
g.setColor(attr.getColor() != null ? attr.getColor() : caretColor);
boolean isRtl = location.myIsRtl;
if (myEditor.isInsertMode() != settings.isBlockCursor()) {
int lineWidth = JBUI.scale(attr.getWidth(settings.getLineCursorWidth()));
// fully cover extra character's pixel which can appear due to antialiasing
// see IDEA-148843 for more details
if (x > minX && lineWidth > 1) x -= 1 / JBUI.sysScale(g);
g.fill(new Rectangle2D.Float(x, y, lineWidth, nominalLineHeight));
if (myDocument.getTextLength() > 0 && caret != null &&
!myView.getTextLayoutCache().getLineLayout(caret.getLogicalPosition().line).isLtr()) {
GeneralPath triangle = new GeneralPath(Path2D.WIND_NON_ZERO, 3);
triangle.moveTo(isRtl ? x + lineWidth : x, y);
triangle.lineTo(isRtl ? x + lineWidth - CARET_DIRECTION_MARK_SIZE : x + CARET_DIRECTION_MARK_SIZE, y);
triangle.lineTo(isRtl ? x + lineWidth : x, y + CARET_DIRECTION_MARK_SIZE);
triangle.closePath();
g.fill(triangle);
}
}
else {
int width = location.myWidth;
float startX = Math.max(minX, isRtl ? x - width : x);
g.fill(new Rectangle2D.Float(startX, y, width, nominalLineHeight - 1));
if (myDocument.getTextLength() > 0 && caret != null) {
int charCount = DocumentUtil.isSurrogatePair(myDocument, caret.getOffset()) ? 2 : 1;
int targetVisualColumn = caret.getVisualPosition().column;
for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView,
caret.getVisualLineStart(),
false)) {
if (fragment.getCurrentInlays() != null) continue;
int startVisualColumn = fragment.getStartVisualColumn();
int endVisualColumn = fragment.getEndVisualColumn();
if (startVisualColumn < targetVisualColumn && endVisualColumn > targetVisualColumn ||
startVisualColumn == targetVisualColumn && !isRtl ||
endVisualColumn == targetVisualColumn && isRtl) {
g.setColor(ColorUtil.isDark(caretColor) ? CARET_LIGHT : CARET_DARK);
fragment.draw(g, startX, y + topOverhang + myView.getAscent(),
targetVisualColumn - startVisualColumn - (isRtl ? charCount : 0),
targetVisualColumn - startVisualColumn + (isRtl ? 0 : charCount));
break;
}
}
ComplexTextFragment.flushDrawingCache(g);
}
}
}
}
void repaintCarets() {
EditorImpl.CaretRectangle[] locations = myEditor.getCaretLocations(false);
if (locations == null) return;
int nominalLineHeight = myView.getNominalLineHeight();
int topOverhang = myView.getTopOverhang();
for (EditorImpl.CaretRectangle location : locations) {
int x = location.myPoint.x;
int y = location.myPoint.y - topOverhang;
int width = Math.max(location.myWidth, CARET_DIRECTION_MARK_SIZE);
myEditor.getContentComponent().repaintEditorComponent(x - width, y, width * 2, nominalLineHeight);
}
}
private void paintLineFragments(Graphics2D g, Rectangle clip, VisualLinesIterator visLineIterator, IterationState.CaretData caretData,
int y, LineFragmentPainter painter) {
int visualLine = visLineIterator.getVisualLine();
float x = getMinX() + (visualLine == 0 ? myView.getPrefixTextWidthInPixels() : 0);
int offset = visLineIterator.getVisualLineStartOffset();
int visualLineEndOffset = visLineIterator.getVisualLineEndOffset();
IterationState it = null;
int prevEndOffset = -1;
boolean firstFragment = true;
int maxColumn = 0;
for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visLineIterator, null)) {
int fragmentStartOffset = fragment.getStartOffset();
int start = fragmentStartOffset;
int end = fragment.getEndOffset();
x = fragment.getStartX();
if (firstFragment) {
firstFragment = false;
SoftWrap softWrap = myEditor.getSoftWrapModel().getSoftWrap(offset);
if (softWrap != null) {
prevEndOffset = offset;
it = new IterationState(myEditor, offset == 0 ? 0 : DocumentUtil.getPreviousCodePointOffset(myDocument, offset), visualLineEndOffset,
caretData, false, false, false, false);
if (it.getEndOffset() <= offset) {
it.advance();
}
if (x >= clip.getMinX()) {
painter.paintBeforeLineStart(g, it.getStartOffset() == offset ? it.getBeforeLineStartBackgroundAttributes() :
it.getMergedAttributes(), fragment.getStartVisualColumn(), x, y);
}
}
}
FoldRegion foldRegion = fragment.getCurrentFoldRegion();
if (foldRegion == null) {
if (start != prevEndOffset) {
it = new IterationState(myEditor, start, fragment.isRtl() ? offset : visualLineEndOffset,
caretData, false, false, false, fragment.isRtl());
}
prevEndOffset = end;
assert it != null;
if (start == end) { // special case of inlays
if (start == it.getEndOffset() && !it.atEnd()) {
it.advance();
}
TextAttributes attributes = it.getStartOffset() == start ? it.getBreakAttributes() : it.getMergedAttributes();
float xNew = fragment.getEndX();
if (xNew >= clip.getMinX()) {
painter.paint(g, fragment, 0, 0, attributes, x, xNew, y);
}
x = xNew;
}
else {
while (fragment.isRtl() ? start > end : start < end) {
if (fragment.isRtl() ? it.getEndOffset() >= start : it.getEndOffset() <= start) {
assert !it.atEnd();
it.advance();
}
TextAttributes attributes = it.getMergedAttributes();
int curEnd = fragment.isRtl() ? Math.max(it.getEndOffset(), end) : Math.min(it.getEndOffset(), end);
float xNew = fragment.offsetToX(x, start, curEnd);
if (xNew >= clip.getMinX()) {
painter.paint(g, fragment,
fragment.isRtl() ? fragmentStartOffset - start : start - fragmentStartOffset,
fragment.isRtl() ? fragmentStartOffset - curEnd : curEnd - fragmentStartOffset,
attributes, x, xNew, y);
}
x = xNew;
start = curEnd;
}
}
}
else {
float xNew = fragment.getEndX();
if (xNew >= clip.getMinX()) {
painter.paint(g, fragment, 0, fragment.getEndVisualColumn() - fragment.getStartVisualColumn(),
getFoldRegionAttributes(foldRegion), x, xNew, y);
}
x = xNew;
prevEndOffset = -1;
it = null;
}
if (x > clip.getMaxX()) return;
maxColumn = fragment.getEndVisualColumn();
}
if (it == null || it.getEndOffset() != visualLineEndOffset) {
it = new IterationState(myEditor, visualLineEndOffset == offset ? visualLineEndOffset
: DocumentUtil.getPreviousCodePointOffset(myDocument, visualLineEndOffset),
visualLineEndOffset, caretData, false, false, false, false);
}
if (!it.atEnd()) {
it.advance();
}
assert it.atEnd();
painter.paintAfterLineEnd(g, clip, it, maxColumn, x, y);
}
private TextAttributes getFoldRegionAttributes(FoldRegion foldRegion) {
TextAttributes selectionAttributes = isSelected(foldRegion) ? myEditor.getSelectionModel().getTextAttributes() : null;
TextAttributes foldAttributes = myEditor.getFoldingModel().getPlaceholderAttributes();
TextAttributes defaultAttributes = getDefaultAttributes();
return mergeAttributes(mergeAttributes(selectionAttributes, foldAttributes), defaultAttributes);
}
@SuppressWarnings("UseJBColor")
private TextAttributes getDefaultAttributes() {
TextAttributes attributes = myEditor.getColorsScheme().getAttributes(HighlighterColors.TEXT);
if (attributes.getForegroundColor() == null) attributes.setForegroundColor(Color.black);
if (attributes.getBackgroundColor() == null) attributes.setBackgroundColor(Color.white);
return attributes;
}
private static boolean isSelected(FoldRegion foldRegion) {
int regionStart = foldRegion.getStartOffset();
int regionEnd = foldRegion.getEndOffset();
int[] selectionStarts = foldRegion.getEditor().getSelectionModel().getBlockSelectionStarts();
int[] selectionEnds = foldRegion.getEditor().getSelectionModel().getBlockSelectionEnds();
for (int i = 0; i < selectionStarts.length; i++) {
int start = selectionStarts[i];
int end = selectionEnds[i];
if (regionStart >= start && regionEnd <= end) return true;
}
return false;
}
private static TextAttributes mergeAttributes(TextAttributes primary, TextAttributes secondary) {
if (primary == null) return secondary;
if (secondary == null) return primary;
return new TextAttributes(primary.getForegroundColor() == null ? secondary.getForegroundColor() : primary.getForegroundColor(),
primary.getBackgroundColor() == null ? secondary.getBackgroundColor() : primary.getBackgroundColor(),
primary.getEffectColor() == null ? secondary.getEffectColor() : primary.getEffectColor(),
primary.getEffectType() == null ? secondary.getEffectType() : primary.getEffectType(),
primary.getFontType() == Font.PLAIN ? secondary.getFontType() : primary.getFontType());
}
@Override
public void drawChars(@NotNull Graphics g, @NotNull char[] data, int start, int end, int x, int y, Color color, FontInfo fontInfo) {
g.setFont(fontInfo.getFont());
g.setColor(color);
g.drawChars(data, start, end - start, x, y);
}
interface LineFragmentPainter {
void paintBeforeLineStart(Graphics2D g, TextAttributes attributes, int columnEnd, float xEnd, int y);
void paint(Graphics2D g, VisualLineFragmentsIterator.Fragment fragment, int start, int end, TextAttributes attributes,
float xStart, float xEnd, int y);
void paintAfterLineEnd(Graphics2D g, Rectangle clip, IterationState iterationState, int columnStart, float x, int y);
}
private static class LineWhitespacePaintingStrategy {
private final boolean myWhitespaceShown;
private final boolean myLeadingWhitespaceShown;
private final boolean myInnerWhitespaceShown;
private final boolean myTrailingWhitespaceShown;
// Offsets on current line where leading whitespace ends and trailing whitespace starts correspondingly.
private int currentLeadingEdge;
private int currentTrailingEdge;
public LineWhitespacePaintingStrategy(EditorSettings settings) {
myWhitespaceShown = settings.isWhitespacesShown();
myLeadingWhitespaceShown = settings.isLeadingWhitespaceShown();
myInnerWhitespaceShown = settings.isInnerWhitespaceShown();
myTrailingWhitespaceShown = settings.isTrailingWhitespaceShown();
}
private void update(CharSequence chars, int lineStart, int lineEnd) {
if (myWhitespaceShown
&& (myLeadingWhitespaceShown || myInnerWhitespaceShown || myTrailingWhitespaceShown)
&& !(myLeadingWhitespaceShown && myInnerWhitespaceShown && myTrailingWhitespaceShown)) {
currentTrailingEdge = CharArrayUtil.shiftBackward(chars, lineStart, lineEnd - 1, WHITESPACE_CHARS) + 1;
currentLeadingEdge = CharArrayUtil.shiftForward(chars, lineStart, currentTrailingEdge, WHITESPACE_CHARS);
}
}
private boolean showWhitespaceAtOffset(int offset) {
return myWhitespaceShown
&& (offset < currentLeadingEdge ? myLeadingWhitespaceShown :
offset >= currentTrailingEdge ? myTrailingWhitespaceShown :
myInnerWhitespaceShown);
}
}
}