void (float x, int y, int start, int end, LineWhitespacePaintingStrategy whitespacePaintingStrategy, VisualLineFragmentsIterator.Fragment fragment, BasicStroke stroke, float scale) { if (!whitespacePaintingStrategy.showAnyWhitespace()) return; boolean restoreStroke = false; Stroke defaultStroke = myGraphics.getStroke(); Color whitespacesColor = myEditor.getColorsScheme().getColor(EditorColors.WHITESPACES_COLOR); Color tabsColor = myEditor.getColorsScheme().getColor(EditorColors.TABS_COLOR); boolean isRtl = fragment.isRtl(); int baseStartOffset = fragment.getStartOffset(); int startOffset = isRtl ? baseStartOffset - start : baseStartOffset + start; int yToUse = y - 1; for (int i = start; i < end; i++) { int charOffset = isRtl ? baseStartOffset - i - 1 : baseStartOffset + i; char c = myText.charAt(charOffset); if (" \t\u3000".indexOf(c) >= 0 && whitespacePaintingStrategy.showWhitespaceAtOffset(charOffset, myCaretDataInView)) { 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 == ' ') { // making center point lie at the center of device pixel float dotX = roundToPixelCenter((startX + endX) / 2.) - scale / 2; float dotY = roundToPixelCenter(yToUse + 1 - myAscent + myLineHeight / 2.) - scale / 2; myTextDrawingTasks.add(g -> { CachingPainter.paint(g, dotX, dotY, scale, scale, _g -> { _g.setColor(whitespacesColor); _g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); _g.fill(new Ellipse2D.Float(0, 0, scale, scale)); }, ourCachedDot, whitespacesColor); }); } else if (c == '\t') { double strokeWidth = Math.max(scale, PaintUtil.devPixel(myGraphics)); switch (AdvancedSettings.getEnum(EDITOR_TAB_PAINTING, TabCharacterPaintMode.class)) { case LONG_ARROW -> { int tabEndX = endX - (int)(myView.getPlainSpaceWidth() / 4); int height = myView.getCharHeight(); Color tabColor = tabsColor == null ? null : ColorUtil.mix(myBackgroundColor, tabsColor, 0.7); myTextDrawingTasks.add(g -> { int halfHeight = height / 2; int yMid = yToUse - halfHeight; int yTop = yToUse - height; g.setColor(tabColor); LinePainter2D.paint(g, startX, yMid, tabEndX, yMid, LinePainter2D.StrokeType.INSIDE, strokeWidth); LinePainter2D.paint(g, tabEndX, yToUse, tabEndX, yTop, LinePainter2D.StrokeType.INSIDE, strokeWidth); g.fillPolygon(new int[]{tabEndX - halfHeight, tabEndX - halfHeight, tabEndX}, new int[]{yToUse, yTop, yMid}, 3); }); } case ARROW -> { int tabLineHeight = calcFeatureSize(4, scale); int tabLineWidth = Math.min(endX - startX, calcFeatureSize(3, scale)); int xToUse = Math.min(endX - tabLineWidth, startX + tabLineWidth); myTextDrawingTasks.add(g -> { g.setColor(tabsColor); g.setStroke(stroke); Object oldHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.drawLine(xToUse, yToUse, xToUse + tabLineWidth, yToUse - tabLineHeight); g.drawLine(xToUse, yToUse - tabLineHeight * 2, xToUse + tabLineWidth, yToUse - tabLineHeight); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldHint); }); restoreStroke = true; } default -> { int yMid = yToUse - myView.getCharHeight() / 2; int tabEndX = Math.max(startX + 1, endX - getTabGap(scale)); myTextDrawingTasks.add(g -> { g.setColor(tabsColor); LinePainter2D.paint(g, startX, yMid, tabEndX, yMid, LinePainter2D.StrokeType.INSIDE, strokeWidth); }); } } } else if (c == '\u3000') { // ideographic space int charHeight = myView.getCharHeight(); int strokeWidth = Math.round(stroke.getLineWidth()); myTextDrawingTasks.add(g -> { g.setColor(whitespacesColor); g.setStroke(stroke); g.drawRect(startX + JBUIScale.scale(2) + strokeWidth / 2, yToUse - charHeight + strokeWidth / 2, endX - startX - JBUIScale.scale(4) - (strokeWidth - 1), charHeight - (strokeWidth - 1)); }); restoreStroke = true; } } } if (restoreStroke) { myTextDrawingTasks.add((g) -> { g.setStroke(defaultStroke); }); } }