EA-87317 - IAE: UIUtil.createImageForGraphics
[idea/community.git] / platform / platform-api / src / com / intellij / ui / paint / EffectPainter.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.ui.paint;
17
18 import com.intellij.openapi.util.registry.Registry;
19 import com.intellij.util.JBHiDPIScaledImage;
20 import com.intellij.util.ui.JBUI;
21 import com.intellij.util.ui.RegionPainter;
22 import com.intellij.util.ui.UIUtil;
23 import com.intellij.util.ui.WavePainter;
24
25 import java.awt.*;
26 import java.awt.font.LineMetrics;
27 import java.awt.geom.*;
28 import java.awt.image.BufferedImage;
29 import java.util.concurrent.ConcurrentHashMap;
30
31 /**
32  * @author Sergey.Malenkov
33  */
34 public enum EffectPainter implements RegionPainter<Font> {
35   /**
36    * @see com.intellij.openapi.editor.markup.EffectType#LINE_UNDERSCORE
37    */
38   LINE_UNDERSCORE {
39     /**
40      * Draws a horizontal line under a text.
41      *
42      * @param g      the {@code Graphics2D} object to render to
43      * @param x      text position
44      * @param y      text baseline
45      * @param width  text width
46      * @param height available space under text
47      * @param font   optional font to calculate line metrics
48      */
49     @Override
50     public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
51       if (!Registry.is("ide.text.effect.new")) {
52         g.drawLine(x, y + 1, x + width, y + 1);
53       }
54       else {
55         paintUnderline(g, x, y, width, height, font, 1, this);
56       }
57     }
58   },
59   /**
60    * @see com.intellij.openapi.editor.markup.EffectType#BOLD_LINE_UNDERSCORE
61    */
62   BOLD_LINE_UNDERSCORE {
63     /**
64      * Draws a bold horizontal line under a text.
65      *
66      * @param g      the {@code Graphics2D} object to render to
67      * @param x      text position
68      * @param y      text baseline
69      * @param width  text width
70      * @param height available space under text
71      * @param font   optional font to calculate line metrics
72      */
73     @Override
74     public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
75       if (!Registry.is("ide.text.effect.new")) {
76         int h = JBUI.scale(Registry.intValue("editor.bold.underline.height", 2));
77         g.fillRect(x, y, width, h);
78       }
79       else {
80         paintUnderline(g, x, y, width, height, font, 2, this);
81       }
82     }
83   },
84   /**
85    * @see com.intellij.openapi.editor.markup.EffectType#BOLD_DOTTED_LINE
86    */
87   BOLD_DOTTED_UNDERSCORE {
88     /**
89      * Draws a bold horizontal line of dots under a text.
90      *
91      * @param g      the {@code Graphics2D} object to render to
92      * @param x      text position
93      * @param y      text baseline
94      * @param width  text width
95      * @param height available space under text
96      * @param font   optional font to calculate line metrics
97      */
98     @Override
99     public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
100       paintUnderline(g, x, y, width, height, font, 2, this);
101     }
102   },
103   /**
104    * @see com.intellij.openapi.editor.markup.EffectType#WAVE_UNDERSCORE
105    */
106   WAVE_UNDERSCORE {
107     /**
108      * Draws a horizontal wave under a text.
109      *
110      * @param g      the {@code Graphics2D} object to render to
111      * @param x      text position
112      * @param y      text baseline
113      * @param width  text width
114      * @param height available space under text
115      * @param font   optional font to calculate line metrics
116      */
117     @Override
118     public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
119       if (!Registry.is("ide.text.effect.new")) {
120         WavePainter.forColor(g.getColor()).paint(g, x, x + width, y + height);
121       }
122       else if (Registry.is("ide.text.effect.new.metrics")) {
123         paintUnderline(g, x, y, width, height, font, 3, this);
124       }
125       else if (width > 0 && height > 0) {
126         Cached.WAVE_UNDERSCORE.paint(g, x, y, width, height, null);
127       }
128     }
129   },
130   /**
131    * @see com.intellij.openapi.editor.markup.EffectType#STRIKEOUT
132    */
133   STRIKE_THROUGH {
134     /**
135      * Draws a horizontal line through a text.
136      *
137      * @param g      the {@code Graphics2D} object to render to
138      * @param x      text position
139      * @param y      text baseline
140      * @param width  text width
141      * @param height text height
142      * @param font   optional font to calculate line metrics
143      */
144     @Override
145     public void paint(Graphics2D g, int x, int y, int width, int height, Font font) {
146       if (width > 0 && height > 0) {
147         if (!Registry.is("ide.text.effect.new.metrics")) {
148           drawLineCentered(g, x, y - height, width, height, 1, this);
149         }
150         else {
151           if (font == null) font = g.getFont();
152           LineMetrics metrics = font.getLineMetrics("", g.getFontRenderContext());
153           int offset = (int)(0.5 - metrics.getStrikethroughOffset());
154           int thickness = Math.max(1, (int)(0.5 + metrics.getStrikethroughThickness()));
155           drawLine(g, x, y - offset, width, thickness, this);
156         }
157       }
158     }
159   };
160
161   private static int getMaxHeight(int height) {
162     return height > 7 && Registry.is("ide.text.effect.new.scale") ? height >> 1 : 3;
163   }
164
165   private static void paintUnderline(Graphics2D g, int x, int y, int width, int height, Font font, int thickness, EffectPainter painter) {
166     if (width > 0 && height > 0) {
167       if (Registry.is("ide.text.effect.new.metrics")) {
168         if (font == null) font = g.getFont();
169         LineMetrics metrics = font.getLineMetrics("", g.getFontRenderContext());
170         thickness = Math.max(thickness, (int)(0.5 + thickness * metrics.getUnderlineThickness()));
171         int offset = Math.min(height - thickness, Math.max(1, (int)(0.5 + metrics.getUnderlineOffset())));
172         if (offset < 1) {
173           offset = height > 3 ? 1 : 0;
174           thickness = height - offset;
175         }
176         drawLine(g, x, y + offset, width, thickness, painter);
177       }
178       else {
179         if (height > 3) {
180           int max = getMaxHeight(height);
181           y += height - max;
182           height = max;
183           if (thickness > 1 && height > 3) {
184             thickness = JBUI.scale(thickness);
185           }
186         }
187         drawLineCentered(g, x, y, width, height, thickness, painter);
188       }
189     }
190   }
191
192   private static void drawLineCentered(Graphics2D g, int x, int y, int width, int height, int thickness, EffectPainter painter) {
193     int offset = height - thickness;
194     if (offset > 0) {
195       y += offset - (offset >> 1);
196       height = thickness;
197     }
198     drawLine(g, x, y, width, height, painter);
199   }
200
201   private static void drawLine(Graphics2D g, int x, int y, int width, int height, EffectPainter painter) {
202     if (painter == BOLD_DOTTED_UNDERSCORE) {
203       int dx = (x % height + height) % height;
204       int w = width + dx;
205       int dw = (w % height + height) % height;
206       Cached.BOLD_DOTTED_UNDERSCORE.paint(g, x - dx, y, dw == 0 ? w : w - dw + height, height, null);
207     }
208     else if (painter == WAVE_UNDERSCORE) {
209       Cached.WAVE_UNDERSCORE.paint(g, x, y, width, height, null);
210     }
211     else {
212       g.fillRect(x, y, width, height);
213     }
214   }
215
216   private enum Cached implements RegionPainter<Paint> {
217     BOLD_DOTTED_UNDERSCORE {
218       @Override
219       int getPeriod(int height) {
220         return height;
221       }
222
223       @Override
224       void paintImage(Graphics2D g, int width, int height, int period) {
225         Integer round = period <= 2 && !UIUtil.isRetina(g) ? null : period;
226         for (int dx = 0; dx < width; dx += period + period) {
227           RectanglePainter.FILL.paint(g, dx, 0, period, period, round);
228         }
229       }
230     },
231     WAVE_UNDERSCORE {
232       private final BasicStroke THIN_STROKE = new BasicStroke(.7f);
233
234       @Override
235       int getPeriod(int height) {
236         return Math.max((Registry.is("ide.text.effect.new.metrics") ? height : getMaxHeight(height)) - 1, 1);
237       }
238
239       @Override
240       void paintImage(Graphics2D g, int width, int height, int period) {
241         double dx = 0;
242         double lower = height - 1;
243         double upper = lower - period;
244         if (Registry.is("ide.text.effect.new.metrics")) {
245           if (height > 3) {
246             float fix = height / 3f;
247             g.setStroke(new BasicStroke(fix));
248             if (fix > 1) {
249               fix = (fix - 1) / 2;
250               lower -= fix;
251               upper += fix;
252             }
253           }
254           height += 2;
255           if (g.getClass().getName().equals("com.intellij.util.HiDPIScaledGraphics")) {
256             lower += .5;
257             upper += .5;
258           }
259         }
260         Path2D path = new Path2D.Double();
261         path.moveTo(dx, lower);
262         if (height < 6) {
263           g.setStroke(THIN_STROKE);
264           while (dx < width) {
265             path.lineTo(dx += period, upper);
266             path.lineTo(dx += period, lower);
267           }
268         }
269         else {
270           double size = (double)period / 2;
271           double prev = dx - size / 2;
272           double center = (upper + lower) / 2;
273           while (dx < width) {
274             path.quadTo(prev += size, lower, dx += size, center);
275             path.quadTo(prev += size, upper, dx += size, upper);
276             path.quadTo(prev += size, upper, dx += size, center);
277             path.quadTo(prev += size, lower, dx += size, lower);
278           }
279         }
280         path.lineTo((double)width, lower);
281         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
282         g.draw(path);
283       }
284     };
285
286     // we should not recalculate caches when IDEA is on Retina and non-Retina
287     private final ConcurrentHashMap<Long, BufferedImage> myNormalCache = new ConcurrentHashMap<>();
288     private final ConcurrentHashMap<Long, BufferedImage> myRetinaCache = new ConcurrentHashMap<>();
289
290     abstract int getPeriod(int height);
291
292     abstract void paintImage(Graphics2D g, int width, int height, int period);
293
294     void paintImage(Graphics2D g, Paint paint, int width, int height, int period) {
295       try {
296         g.setPaint(paint);
297         paintImage(g, width, height, period);
298       }
299       finally {
300         g.dispose();
301       }
302     }
303
304     BufferedImage getImage(Graphics2D g, Color color, int height) {
305       Long key = color.getRGB() ^ ((long)height << 32);
306       ConcurrentHashMap<Long, BufferedImage> cache = UIUtil.isRetina(g) ? myRetinaCache : myNormalCache;
307       return cache.computeIfAbsent(key, k -> createImage(g, color, height));
308     }
309
310     BufferedImage createImage(Graphics2D g, Paint paint, int height) {
311       int period = getPeriod(height);
312       int width = period << (paint instanceof Color ? 8 : 1);
313       BufferedImage image = UIUtil.createImageForGraphics(g, width, height, BufferedImage.TYPE_INT_ARGB);
314       paintImage(image.createGraphics(), paint, width, height, period);
315       return image;
316     }
317
318     @Override
319     public void paint(Graphics2D g, int x, int y, int width, int height, Paint paint) {
320       if (paint == null) paint = g.getPaint();
321       g = (Graphics2D)g.create(x, y, width, height);
322       g.setComposite(AlphaComposite.SrcOver);
323       BufferedImage image = paint instanceof Color ? getImage(g, (Color)paint, height) : createImage(g, paint, height);
324       int period = image.getWidth(null);
325       if (image instanceof JBHiDPIScaledImage) period /= 2;
326       int offset = (x % period + period) % period; // normalize
327       for (int dx = -offset; dx < width; dx += period) {
328         UIUtil.drawImage(g, image, dx, 0, null);
329       }
330       g.dispose();
331     }
332   }
333 }