| /*=============================================================================# |
| # Copyright (c) 2011, 2020 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.internal.rj.eclient.graphics; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.graphics.Device; |
| import org.eclipse.swt.graphics.Font; |
| import org.eclipse.swt.graphics.FontData; |
| import org.eclipse.swt.graphics.FontMetrics; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.ImageData; |
| import org.eclipse.swt.widgets.Display; |
| |
| |
| public class FontManager { |
| |
| private static final int[] R2SWT_STYLE= new int[] { |
| SWT.NONE, |
| SWT.BOLD, |
| SWT.ITALIC, |
| SWT.BOLD | SWT.ITALIC, |
| }; |
| private static final int R_STYLE_COUNT= 4; |
| |
| |
| private static final int HR_FONTSIZE= 60; |
| private static final double HR_FACTOR= HR_FONTSIZE; |
| private static final int HR_MIN_FONTSIZE= HR_FONTSIZE / 2; |
| |
| |
| private static final class FontInstance { |
| |
| private static final double[] UNINITIALIZED= new double[0]; |
| |
| public final int size; |
| public final Font swtFont; |
| |
| private double[] swtFontProperties; // [ baselineOffset ] -> FontSetting |
| |
| private int baseLine; |
| private double metricTolerance= 0; |
| private double ascentUp= Integer.MIN_VALUE; |
| private double ascentLow= Integer.MIN_VALUE; |
| |
| private int[] charAbove255; |
| private double[] charMetrics= UNINITIALIZED; |
| private double[] strWidth= UNINITIALIZED; |
| |
| |
| public FontInstance(final int size, final Font font) { |
| this.size= size; |
| this.swtFont= font; |
| } |
| |
| |
| public void init(final GC gc) { |
| final FontMetrics fontMetrics= gc.getFontMetrics(); |
| this.baseLine= fontMetrics.getLeading() + fontMetrics.getAscent(); |
| this.metricTolerance= fontMetrics.getAscent() * 0.05; |
| |
| this.swtFontProperties= new double[] { |
| this.baseLine, |
| }; |
| } |
| |
| public int checkChar(final int ch) { |
| if (ch <= 0) { |
| return 0; |
| } |
| else if (ch <= 255) { |
| return ch; |
| } |
| else if (this.charAbove255 == null){ |
| this.charAbove255= new int[256]; |
| this.charAbove255[0]= ch; |
| return 256 + 0; |
| } |
| else { |
| for (int i= 0; i < this.charAbove255.length; i++) { |
| final int c= this.charAbove255[i]; |
| if (c == 0) { |
| this.charAbove255[i]= c; |
| return 256 + i; |
| } |
| else if (c == ch) { |
| return 256 + i; |
| } |
| } |
| final int[] newChar= new int[this.charAbove255.length + 256]; |
| System.arraycopy(this.charAbove255, 0, newChar, 0, this.charAbove255.length); |
| newChar[this.charAbove255.length]= ch; |
| this.charAbove255= newChar; |
| return this.charAbove255.length - 256; |
| } |
| } |
| |
| } |
| |
| |
| private static final class TestGC { |
| |
| |
| private final GC testGC; |
| |
| private FontInstance font; |
| |
| private final Image image; |
| private final int imageWidth; |
| private final int imageHeigth; |
| private final int imagePixelBytes; |
| private final int imageLineBytes; |
| private final byte imageBlankData; |
| |
| private ImageData imageData; |
| |
| |
| public TestGC(final Device device) { |
| final GC tempGC= new GC(device); |
| final Font tempFont= new Font(device, new FontData(device.getSystemFont().getFontData()[0].getName(), HR_FONTSIZE, 0)); |
| tempGC.setFont(tempFont); |
| this.imageHeigth= (int) (tempGC.getFontMetrics().getHeight() * 1.5); |
| this.imageWidth= this.imageHeigth * 2; |
| tempGC.dispose(); |
| tempFont.dispose(); |
| |
| this.image= new Image(device, this.imageWidth, this.imageHeigth); |
| |
| this.testGC= new GC(this.image); |
| this.testGC.setAdvanced(true); |
| this.testGC.setAntialias(SWT.ON); |
| this.testGC.setTextAntialias(SWT.ON); |
| this.testGC.setInterpolation(SWT.HIGH); |
| this.testGC.setAlpha(255); |
| this.testGC.setBackground(device.getSystemColor(SWT.COLOR_WHITE)); |
| this.testGC.setForeground(device.getSystemColor(SWT.COLOR_BLACK)); |
| this.testGC.setFont(null); |
| this.font= null; |
| |
| clearImage(); |
| this.imageData= this.image.getImageData(); |
| this.imageLineBytes= this.imageData.bytesPerLine; |
| this.imagePixelBytes= Math.max(this.imageData.bytesPerLine / this.imageData.width, 1); |
| this.imageBlankData= this.imageData.data[0]; |
| } |
| |
| |
| public void setFont(final FontInstance font) { |
| // if (gcFont != font) { // E-Bug #319125 |
| this.font= font; |
| this.testGC.setFont(font.swtFont); |
| // } |
| if (this.font.baseLine == 0) { |
| this.font.init(this.testGC); |
| this.testGC.setFont(font.swtFont); // E-Bug #319125 |
| } |
| } |
| |
| public FontInstance getFont() { |
| return this.font; |
| } |
| |
| public int getStringWidth(final String txt) { |
| return this.testGC.stringExtent(txt).x; |
| } |
| |
| public void clearImage() { |
| this.testGC.fillRectangle(0, 0, this.imageWidth, this.imageHeigth); |
| this.imageData= null; |
| } |
| |
| private void drawText(final String txt) { |
| this.testGC.drawString(txt, 0, 0, true); |
| } |
| |
| public int findFirstLine() { |
| if (this.imageData == null) { |
| this.imageData= this.image.getImageData(); |
| } |
| final byte[] data= this.imageData.data; |
| for (int i= 0; i < data.length; i+= this.imagePixelBytes) { |
| if (data[i] != this.imageBlankData) { |
| return (i / this.imageLineBytes); |
| } |
| } |
| return -1; |
| } |
| |
| public int findLastLine() { |
| if (this.imageData == null) { |
| this.imageData= this.image.getImageData(); |
| } |
| final byte[] data= this.imageData.data; |
| for (int i= data.length - this.imagePixelBytes; i >= 0; i-= this.imagePixelBytes) { |
| if (data[i] != this.imageBlankData) { |
| return (i / this.imageLineBytes); |
| } |
| } |
| return -1; |
| } |
| |
| |
| public void dispose() { |
| if (!this.testGC.isDisposed()) { |
| this.testGC.dispose(); |
| } |
| if (!this.image.isDisposed()) { |
| this.image.dispose(); |
| } |
| } |
| |
| } |
| |
| |
| public final class FontFamily { |
| |
| private static final double POLISH_SMALL_MAX_FACTOR= 0.4; |
| private static final double POLISH_SMALL_ADD_CORR= 0.04; |
| private static final double POLISH_ADD_CONST= 0.05; |
| private static final double POLISH_ADD_REL= 0.1; |
| |
| private static final int METRIC_IDX_ADVWIDTH= 0; |
| private static final int METRIC_IDX_ASCENT= 1; |
| private static final int METRIC_IDX_DESCENT= 2; |
| private static final int METRIC_COUNT= 3; |
| |
| |
| final String name; |
| |
| final FontInstance[][] fonts= new FontInstance[R_STYLE_COUNT][]; |
| |
| |
| private FontFamily(final String name) { |
| this.name= name; |
| } |
| |
| |
| private FontInstance get(final int style, final int size) { |
| FontInstance[] styleFonts= this.fonts[style]; |
| if (styleFonts == null) { |
| this.fonts[style]= styleFonts= new FontInstance[4]; |
| } |
| int idx; |
| if (size == HR_FONTSIZE) { |
| idx= 0; |
| } |
| else { |
| idx= styleFonts.length; |
| for (int i= 1; i < styleFonts.length; i++) { |
| if (styleFonts[i] != null) { |
| if (styleFonts[i].size == size) { |
| idx= i; |
| break; |
| } |
| } |
| else { |
| idx= i; |
| break; |
| } |
| } |
| if (idx >= styleFonts.length) { |
| this.fonts[style]= new FontInstance[styleFonts.length+4]; |
| System.arraycopy(styleFonts, 0, this.fonts[style], 0, styleFonts.length); |
| styleFonts= this.fonts[style]; |
| } |
| } |
| if (styleFonts[idx] == null) { |
| final FontData fontData= new FontData(this.name, size, R2SWT_STYLE[style]); |
| styleFonts[idx]= new FontInstance(size, |
| new Font(FontManager.this.display, fontData)); |
| } |
| return styleFonts[idx]; |
| } |
| |
| public synchronized Font getSWTFont(final int style, final int size) { |
| return get(style, size).swtFont; |
| } |
| |
| public synchronized double[] getCharMetrics(final int style, final int size, final int ch) { |
| final FontInstance font= get(style, HR_FONTSIZE); |
| final double factor= size / HR_FACTOR; |
| final int chIdx= font.checkChar(ch) * METRIC_COUNT; |
| |
| final double[] answer= new double[3]; |
| |
| if (chIdx >= font.charMetrics.length |
| || font.charMetrics[chIdx] == 0) { |
| synchronized (getTestLock()) { |
| final TestGC gc= getTestGC(); |
| gc.setFont(font); |
| |
| if (font.charMetrics.length == 0) { |
| font.charMetrics= new double[Math.max(512 * METRIC_COUNT, |
| chIdx + 128 * METRIC_COUNT)]; |
| |
| font.charMetrics[' ' * METRIC_COUNT + METRIC_IDX_ADVWIDTH] = |
| (gc.testGC.getAdvanceWidth('m') * 0.2 + gc.testGC.getAdvanceWidth(' ') * 1.0) / 2.0; |
| font.charMetrics[' ' * METRIC_COUNT + METRIC_IDX_ASCENT]= 0; |
| font.charMetrics[' ' * METRIC_COUNT + METRIC_IDX_DESCENT]= 0; |
| |
| font.ascentUp= checkCharAscentMean(gc, new char[] { 'A', 'M', 'O', 'E' }); |
| font.ascentLow= checkCharAscentMean(gc, new char[] { 'a', 'm', 'p', 'w' }); |
| |
| font.charMetrics[0 * METRIC_COUNT + METRIC_IDX_ADVWIDTH] = |
| font.charMetrics['M' * METRIC_COUNT + METRIC_IDX_ADVWIDTH]; |
| font.charMetrics[0 * METRIC_COUNT + METRIC_IDX_ASCENT] = |
| (font.ascentUp > 0) ? font.ascentUp : 0; |
| font.charMetrics[0 * METRIC_COUNT + METRIC_IDX_DESCENT]= 0; |
| } |
| else if (chIdx >= font.charMetrics.length) { |
| final double[] newArray= new double[(chIdx + 128 * METRIC_COUNT)]; |
| System.arraycopy(font.charMetrics, 0, newArray, 0, font.charMetrics.length); |
| font.charMetrics= newArray; |
| } |
| |
| if (font.charMetrics[chIdx] == 0) { |
| computeCharHeights(gc, ch, chIdx); |
| } |
| |
| // Point sext= gc.stringExtent(new String(new int[] { this.currentChar }, 0, 1)); |
| // Point text= gc.textExtent(new String(new int[] { this.currentChar }, 0, 1), SWT.TRANSPARENT); |
| // System.out.println("height= " + gc.getHeight()); |
| // System.out.println("leading= " + gc.getLeading()); |
| // System.out.println("ascent= " + gc.getAscent()); |
| // System.out.println("descent= " + gc.getDescent()); |
| // System.out.println("stringExtend.y= " + sext.y); |
| // System.out.println("textExtend.y= " + text.y); |
| // System.out.println("stringExtend.x= " + sext.x); |
| // System.out.println("textExtend.x= " + text.x); |
| // System.out.println("advanceWidth= " + gc.getAdvanceWidth((char) this.currentChar)); |
| // System.out.println("charWidth= " + gc.getCharWidth((char) this.currentChar)); |
| // System.out.println("averageCharWidth= " + gc.getFontMetrics.getAverageCharWidth()); |
| // System.out.println(this.currentAnswer[2]); |
| } |
| } |
| |
| answer[0]= polish(font.charMetrics[chIdx + METRIC_IDX_ASCENT] + 1.01 / factor, factor); |
| answer[1]= polish(font.charMetrics[chIdx + METRIC_IDX_DESCENT], factor); |
| answer[2]= polish(font.charMetrics[chIdx + METRIC_IDX_ADVWIDTH], factor); |
| |
| // System.out.println("-> " + Arrays.toString(answer)); |
| return answer; |
| } |
| |
| private void computeCharHeights(final TestGC gc, final int ch, final int idx) { |
| final FontInstance font= gc.getFont(); |
| font.charMetrics[idx + METRIC_IDX_ADVWIDTH] = |
| gc.testGC.getAdvanceWidth((char) ch); |
| |
| final String s= String.valueOf((char) ch); |
| |
| gc.clearImage(); |
| gc.drawText(s); |
| final int firstLine= gc.findFirstLine(); |
| if (firstLine >= 0) { |
| double ascent= font.baseLine - firstLine; |
| if (Math.abs(ascent - font.ascentUp) <= font.metricTolerance) { |
| ascent= font.ascentUp; |
| } |
| else if (Math.abs(ascent - font.ascentLow) <= font.metricTolerance) { |
| ascent= font.ascentLow; |
| } |
| int descent= gc.findLastLine() - font.baseLine + 1; |
| if (Math.abs(descent) <= font.metricTolerance) { |
| descent= 0; |
| } |
| font.charMetrics[idx + METRIC_IDX_ASCENT]= ascent; |
| font.charMetrics[idx + METRIC_IDX_DESCENT]= descent; |
| } |
| else { |
| font.charMetrics[idx + METRIC_IDX_ASCENT]= 0; |
| font.charMetrics[idx + METRIC_IDX_DESCENT]= 0; |
| } |
| |
| if (idx / METRIC_COUNT < font.strWidth.length |
| && font.strWidth[idx / METRIC_COUNT] == 0) { |
| font.strWidth[idx / METRIC_COUNT]= gc.getStringWidth(s); |
| } |
| } |
| |
| private double checkCharAscentMean(final TestGC gc, final char[] chars) { |
| final FontInstance font= gc.getFont(); |
| double mean= 0; |
| for (int i= 0; i < chars.length; i++) { |
| computeCharHeights(gc, chars[i], chars[i] * METRIC_COUNT); |
| mean+= font.charMetrics[chars[i] * METRIC_COUNT + METRIC_IDX_ASCENT]; |
| gc.testGC.setFont(font.swtFont); // E-Bug #319125 |
| } |
| mean /= chars.length; |
| mean= polish(mean * 2.0, 1.0) / 2.0; |
| for (int i= 0; i < chars.length; i++) { |
| if (Math.abs(mean - font.charMetrics[chars[i] * METRIC_COUNT + METRIC_IDX_ASCENT]) > font.metricTolerance) { |
| return Integer.MIN_VALUE; |
| } |
| } |
| for (int i= 0; i < chars.length; i++) { |
| font.charMetrics[chars[i] * METRIC_COUNT + METRIC_IDX_ASCENT]= mean; |
| } |
| return mean; |
| } |
| |
| public synchronized double[] getStringWidth(final int style, final int size, final String text) { |
| final FontInstance font; |
| final double factor; |
| final int chIdx; |
| if (size < HR_MIN_FONTSIZE && text.length() == 1) { |
| font= get(style, HR_FONTSIZE); |
| factor= size / HR_FACTOR; |
| chIdx= font.checkChar(text.charAt(0)); |
| } |
| else { |
| font= get(style, size); |
| factor= 1.0; |
| chIdx= -1; |
| } |
| |
| final double[] answer= new double[] { (8 * text.length()) }; |
| |
| if (chIdx < 0 |
| || chIdx >= font.strWidth.length |
| || font.strWidth[chIdx] == 0) { |
| synchronized (getTestLock()) { |
| final TestGC gc= getTestGC(); |
| gc.setFont(font); |
| |
| final double width= gc.getStringWidth(text); |
| answer[0]= width; |
| if (chIdx >= 0) { |
| if (font.strWidth.length == 0) { |
| font.strWidth= new double[Math.max(512, chIdx + 256)]; |
| } |
| else if (chIdx >= font.strWidth.length) { |
| final double[] newWidth= new double[chIdx + 128]; |
| System.arraycopy(font.strWidth, 0, newWidth, 0, font.strWidth.length); |
| font.strWidth= newWidth; |
| } |
| font.strWidth[chIdx]= width; |
| } |
| } |
| } |
| else { |
| answer[0]= font.strWidth[chIdx]; |
| } |
| if (factor != 1.0) { |
| // this.currentAnswer[0]= Math.round((this.currentAnswer[0] * (factor * HR_ROUND_FACTOR)) + HR_ROUND_ADD) / HR_ROUND_FACTOR; |
| answer[0]= polish(answer[0], factor); |
| } |
| |
| // System.out.println("-> " + Arrays.toString(answer)); |
| return answer; |
| } |
| |
| public synchronized double[] getSWTFontProperties(final int style, final int size) { |
| final FontInstance font= get(style, size); // no HR! |
| if (font.swtFontProperties == null) { |
| synchronized (getTestLock()) { |
| final TestGC gc= getTestGC(); |
| gc.setFont(font); |
| } |
| } |
| return font.swtFontProperties; |
| } |
| |
| private double polish(final double p, final double factor) { |
| if (p == 0) { |
| return 0; |
| } |
| final double add= (factor < POLISH_SMALL_MAX_FACTOR) ? |
| (POLISH_SMALL_ADD_CORR / factor + POLISH_ADD_CONST) : |
| (POLISH_SMALL_ADD_CORR/POLISH_SMALL_MAX_FACTOR + POLISH_ADD_CONST); |
| if (p > 0) { |
| return Math.round((p + POLISH_ADD_REL) * factor + add); |
| } |
| else { |
| return Math.round((p - POLISH_ADD_REL) * factor - add); |
| } |
| } |
| |
| |
| public void dispose() { |
| for (int style= 0; style < R_STYLE_COUNT; style++) { |
| final FontInstance[] styleFonts= this.fonts[style]; |
| if (styleFonts != null) { |
| for (int i= 0; i < styleFonts.length; i++) { |
| if (styleFonts[i] != null && styleFonts[i].swtFont != null) { |
| if (!styleFonts[i].swtFont.isDisposed()) { |
| styleFonts[i].swtFont.dispose(); |
| } |
| styleFonts[i]= null; |
| } |
| } |
| } |
| } |
| } |
| |
| } |
| |
| |
| private final Display display; |
| |
| private final Object testGCLock= new Object(); |
| private TestGC testGC; |
| private boolean disposed; |
| |
| private final Map<String, FontFamily> fontFamilies= new HashMap<>(); |
| |
| |
| public FontManager(final Display display) { |
| this.display= display; |
| } |
| |
| |
| public synchronized FontFamily getFamily(final String family) { |
| FontFamily fontFamily= this.fontFamilies.get(family); |
| if (fontFamily == null) { |
| fontFamily= new FontFamily(family); |
| this.fontFamilies.put(family, fontFamily); |
| } |
| return fontFamily; |
| } |
| |
| |
| protected final Object getTestLock() { |
| return this.testGCLock; |
| } |
| |
| protected final TestGC getTestGC() { |
| if (this.testGC == null) { |
| this.display.syncExec(new Runnable() { |
| @Override |
| public void run() { |
| if (!FontManager.this.disposed) { |
| FontManager.this.testGC= new TestGC(FontManager.this.display); |
| } |
| } |
| }); |
| } |
| return this.testGC; |
| } |
| |
| |
| public void dispose() { |
| this.disposed= true; |
| for (final FontFamily fontFamily : this.fontFamilies.values()) { |
| fontFamily.dispose(); |
| } |
| this.fontFamilies.clear(); |
| |
| if (this.testGC != null) { |
| this.testGC.dispose(); |
| this.testGC= null; |
| } |
| } |
| |
| } |