| /*=============================================================================# |
| # Copyright (c) 2011, 2021 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.Arrays; |
| 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; |
| |
| import org.eclipse.statet.jcommons.lang.NonNullByDefault; |
| import org.eclipse.statet.jcommons.lang.Nullable; |
| |
| |
| @NonNullByDefault |
| 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 int[] EMPTY_INT_ARRY= new int[0]; |
| |
| |
| 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= EMPTY_INT_ARRY; |
| 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 { |
| int[] charAbove255= this.charAbove255; |
| int i= 0; |
| for (; i < charAbove255.length; i++) { |
| final int c= charAbove255[i]; |
| if (c == ch) { |
| return 256 + i; |
| } |
| if (c == 0) { |
| charAbove255[i]= c; |
| return 256 + i; |
| } |
| } |
| // i == charAbove255.length |
| charAbove255= Arrays.copyOf(charAbove255, i + 256); |
| this.charAbove255= charAbove255; |
| charAbove255[i]= ch; |
| return 256 + i; |
| } |
| } |
| |
| } |
| |
| |
| 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 imagePixelBWidth; |
| private final int imagePixelBIndex; |
| private final int imageLineBytes; |
| private final byte imageBlankData; |
| |
| private @Nullable 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(); |
| final var imageData= this.image.getImageData(); |
| this.imageLineBytes= imageData.bytesPerLine; |
| this.imagePixelBWidth= Math.max(imageData.bytesPerLine / imageData.width, 1); |
| this.imagePixelBIndex= (imageData.palette.isDirect) ? |
| this.imagePixelBWidth - 1 + imageData.palette.redShift / 8 : |
| 0; |
| this.imageBlankData= imageData.data[this.imagePixelBIndex]; |
| } |
| |
| |
| public void setFont(final FontInstance font) { |
| // if (gcFont != font) { // E-Bug #319125 |
| this.font= font; |
| this.testGC.setFont(font.swtFont); |
| // } |
| if (font.baseLine == 0) { |
| 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 ImageData getImageData() { |
| var imageData= this.imageData; |
| if (imageData == null) { |
| imageData= this.image.getImageData(); |
| this.imageData= imageData; |
| } |
| return imageData; |
| } |
| |
| private void drawText(final String txt) { |
| this.testGC.drawString(txt, 0, 0, true); |
| } |
| |
| public int findFirstLine() { |
| final var imageData= getImageData(); |
| final byte[] data= imageData.data; |
| for (int index= this.imagePixelBIndex; |
| index < data.length; index+= this.imagePixelBWidth) { |
| if (data[index] != this.imageBlankData) { |
| return (index / this.imageLineBytes); |
| } |
| } |
| return -1; |
| } |
| |
| public int findLastLine() { |
| final var imageData= getImageData(); |
| final byte[] data= imageData.data; |
| for (int index= data.length - this.imagePixelBWidth + this.imagePixelBIndex; |
| index >= 0; index-= this.imagePixelBWidth) { |
| if (data[index] != this.imageBlankData) { |
| return (index / 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) { |
| styleFonts= new FontInstance[8]; |
| this.fonts[style]= styleFonts; |
| } |
| 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) { |
| styleFonts= Arrays.copyOf(styleFonts, styleFonts.length + 8); |
| this.fonts[style]= styleFonts; |
| } |
| } |
| |
| FontInstance font= styleFonts[idx]; |
| if (font == null) { |
| final FontData fontData= new FontData(this.name, size, R2SWT_STYLE[style]); |
| font= new FontInstance(size, |
| new Font(FontManager.this.display, fontData) ); |
| styleFonts[idx]= font; |
| } |
| return font; |
| } |
| |
| 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 @Nullable 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() { |
| TestGC testGC= this.testGC; |
| if (testGC == null) { |
| this.display.syncExec(() -> { |
| if (!this.disposed) { |
| this.testGC= new TestGC(this.display); |
| } |
| }); |
| testGC= this.testGC; |
| if (testGC == null) { |
| throw new RuntimeException("disposed"); |
| } |
| } |
| return testGC; |
| } |
| |
| |
| public void dispose() { |
| this.disposed= true; |
| for (final FontFamily fontFamily : this.fontFamilies.values()) { |
| fontFamily.dispose(); |
| } |
| this.fontFamilies.clear(); |
| |
| final var testGC= this.testGC; |
| if (testGC != null) { |
| this.testGC= null; |
| testGC.dispose(); |
| } |
| } |
| |
| } |