/*
* 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.ui.paint;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.util.JBHiDPIScaledImage;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.RegionPainter;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.WavePainter;
import java.awt.*;
import java.awt.font.LineMetrics;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author Sergey.Malenkov
*/
public enum EffectPainter implements RegionPainter<Font> {
/**
* @see com.intellij.openapi.editor.markup.EffectType#LINE_UNDERSCORE
*/
LINE_UNDERSCORE {
/**
* Draws a horizontal line under a text.
*
* @param g the {@code Graphics2D} object to render to
* @param x text position
* @param y text baseline
* @param width text width
* @param height available space under text
* @param font optional font to calculate line metrics
*/
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
if (!Registry.is("ide.text.effect.new")) {
g.drawLine(x, y + 1, x + width, y + 1);
}
else {
paintUnderline(g, x, y, width, height, font, 1, this);
}
}
},
/**
* @see com.intellij.openapi.editor.markup.EffectType#BOLD_LINE_UNDERSCORE
*/
BOLD_LINE_UNDERSCORE {
/**
* Draws a bold horizontal line under a text.
*
* @param g the {@code Graphics2D} object to render to
* @param x text position
* @param y text baseline
* @param width text width
* @param height available space under text
* @param font optional font to calculate line metrics
*/
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
if (!Registry.is("ide.text.effect.new")) {
int h = JBUI.scale(Registry.intValue("editor.bold.underline.height", 2));
g.fillRect(x, y, width, h);
}
else {
paintUnderline(g, x, y, width, height, font, 2, this);
}
}
},
/**
* @see com.intellij.openapi.editor.markup.EffectType#BOLD_DOTTED_LINE
*/
BOLD_DOTTED_UNDERSCORE {
/**
* Draws a bold horizontal line of dots under a text.
*
* @param g the {@code Graphics2D} object to render to
* @param x text position
* @param y text baseline
* @param width text width
* @param height available space under text
* @param font optional font to calculate line metrics
*/
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
paintUnderline(g, x, y, width, height, font, 2, this);
}
},
/**
* @see com.intellij.openapi.editor.markup.EffectType#WAVE_UNDERSCORE
*/
WAVE_UNDERSCORE {
/**
* Draws a horizontal wave under a text.
*
* @param g the {@code Graphics2D} object to render to
* @param x text position
* @param y text baseline
* @param width text width
* @param height available space under text
* @param font optional font to calculate line metrics
*/
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
if (!Registry.is("ide.text.effect.new")) {
WavePainter.forColor(g.getColor()).paint(g, x, x + width, y + height);
}
else if (Registry.is("ide.text.effect.new.metrics")) {
paintUnderline(g, x, y, width, height, font, 3, this);
}
else if (width > 0 && height > 0) {
Cached.WAVE_UNDERSCORE.paint(g, x, y, width, height, null);
}
}
},
/**
* @see com.intellij.openapi.editor.markup.EffectType#STRIKEOUT
*/
STRIKE_THROUGH {
/**
* Draws a horizontal line through a text.
*
* @param g the {@code Graphics2D} object to render to
* @param x text position
* @param y text baseline
* @param width text width
* @param height text height
* @param font optional font to calculate line metrics
*/
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
if (width > 0 && height > 0) {
if (!Registry.is("ide.text.effect.new.metrics")) {
drawLineCentered(g, x, y - height, width, height, 1, this);
}
else {
if (font == null) font = g.getFont();
LineMetrics metrics = font.getLineMetrics("", g.getFontRenderContext());
int offset = (int)(0.5 - metrics.getStrikethroughOffset());
int thickness = Math.max(1, (int)(0.5 + metrics.getStrikethroughThickness()));
drawLine(g, x, y - offset, width, thickness, this);
}
}
}
};
private static int getMaxHeight(int height) {
return height > 7 && Registry.is("ide.text.effect.new.scale") ? height >> 1 : 3;
}
private static void paintUnderline(Graphics2D g, int x, int y, int width, int height, Font font, int thickness, EffectPainter painter) {
if (width > 0 && height > 0) {
if (Registry.is("ide.text.effect.new.metrics")) {
if (font == null) font = g.getFont();
LineMetrics metrics = font.getLineMetrics("", g.getFontRenderContext());
thickness = Math.max(thickness, (int)(0.5 + thickness * metrics.getUnderlineThickness()));
int offset = Math.min(height - thickness, Math.max(1, (int)(0.5 + metrics.getUnderlineOffset())));
if (offset < 1) {
offset = height > 3 ? 1 : 0;
thickness = height - offset;
}
drawLine(g, x, y + offset, width, thickness, painter);
}
else {
if (height > 3) {
int max = getMaxHeight(height);
y += height - max;
height = max;
if (thickness > 1 && height > 3) {
thickness = JBUI.scale(thickness);
}
}
drawLineCentered(g, x, y, width, height, thickness, painter);
}
}
}
private static void drawLineCentered(Graphics2D g, int x, int y, int width, int height, int thickness, EffectPainter painter) {
int offset = height - thickness;
if (offset > 0) {
y += offset - (offset >> 1);
height = thickness;
}
drawLine(g, x, y, width, height, painter);
}
private static void drawLine(Graphics2D g, int x, int y, int width, int height, EffectPainter painter) {
if (painter == BOLD_DOTTED_UNDERSCORE) {
int dx = (x % height + height) % height;
int w = width + dx;
int dw = (w % height + height) % height;
Cached.BOLD_DOTTED_UNDERSCORE.paint(g, x - dx, y, dw == 0 ? w : w - dw + height, height, null);
}
else if (painter == WAVE_UNDERSCORE) {
Cached.WAVE_UNDERSCORE.paint(g, x, y, width, height, null);
}
else {
g.fillRect(x, y, width, height);
}
}
private enum Cached implements RegionPainter<Paint> {
BOLD_DOTTED_UNDERSCORE {
@Override
int getPeriod(int height) {
return height;
}
@Override
void paintImage(Graphics2D g, int width, int height, int period) {
Integer round = period <= 2 && !UIUtil.isJreHiDPI(g) ? null : period;
for (int dx = 0; dx < width; dx += period + period) {
RectanglePainter.FILL.paint(g, dx, 0, period, period, round);
}
}
},
WAVE_UNDERSCORE {
private final BasicStroke THIN_STROKE = new BasicStroke(.7f);
@Override
int getPeriod(int height) {
return Math.max((Registry.is("ide.text.effect.new.metrics") ? height : getMaxHeight(height)) - 1, 1);
}
@Override
void paintImage(Graphics2D g, int width, int height, int period) {
double dx = 0;
double lower = height - 1;
double upper = lower - period;
if (Registry.is("ide.text.effect.new.metrics")) {
if (height > 3) {
float fix = height / 3f;
g.setStroke(new BasicStroke(fix));
if (fix > 1) {
fix = (fix - 1) / 2;
lower -= fix;
upper += fix;
}
}
height += 2;
if (g.getClass().getName().equals("com.intellij.util.HiDPIScaledGraphics")) {
lower += .5;
upper += .5;
}
}
Path2D path = new Path2D.Double();
path.moveTo(dx, lower);
if (height < 6) {
g.setStroke(THIN_STROKE);
while (dx < width) {
path.lineTo(dx += period, upper);
path.lineTo(dx += period, lower);
}
}
else {
double size = (double)period / 2;
double prev = dx - size / 2;
double center = (upper + lower) / 2;
while (dx < width) {
path.quadTo(prev += size, lower, dx += size, center);
path.quadTo(prev += size, upper, dx += size, upper);
path.quadTo(prev += size, upper, dx += size, center);
path.quadTo(prev += size, lower, dx += size, lower);
}
}
path.lineTo((double)width, lower);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.draw(path);
}
};
// we should not recalculate caches when IDEA is on Retina and non-Retina
private final ConcurrentHashMap<Long, BufferedImage> myNormalCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, BufferedImage> myRetinaCache = new ConcurrentHashMap<>();
abstract int getPeriod(int height);
abstract void paintImage(Graphics2D g, int width, int height, int period);
void paintImage(Graphics2D g, Paint paint, int width, int height, int period) {
try {
g.setPaint(paint);
paintImage(g, width, height, period);
}
finally {
g.dispose();
}
}
BufferedImage getImage(Graphics2D g, Color color, int height) {
Long key = color.getRGB() ^ ((long)height << 32);
ConcurrentHashMap<Long, BufferedImage> cache = UIUtil.isRetina(g) ? myRetinaCache : myNormalCache;
return cache.computeIfAbsent(key, k -> createImage(g, color, height));
}
BufferedImage createImage(Graphics2D g, Paint paint, int height) {
int period = getPeriod(height);
int width = period << (paint instanceof Color ? 8 : 1);
BufferedImage image = UIUtil.createImage(g, width, height, BufferedImage.TYPE_INT_ARGB);
paintImage(image.createGraphics(), paint, width, height, period);
return image;
}
@Override
public void paint(Graphics2D g, int x, int y, int width, int height, Paint paint) {
if (paint == null) paint = g.getPaint();
g = (Graphics2D)g.create(x, y, width, height);
g.setComposite(AlphaComposite.SrcOver);
BufferedImage image = paint instanceof Color ? getImage(g, (Color)paint, height) : createImage(g, paint, height);
int period = image.getWidth(null);
if (image instanceof JBHiDPIScaledImage) period /= 2;
int offset = (x % period + period) % period; // normalize
for (int dx = -offset; dx < width; dx += period) {
UIUtil.drawImage(g, image, dx, 0, null);
}
g.dispose();
}
}
}