| /*=============================================================================# |
| # Copyright (c) 2011, 2022 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 static org.eclipse.statet.ecommons.ui.swt.AutoDisposeReference.autoDispose; |
| |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.function.BooleanSupplier; |
| |
| 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; |
| import org.eclipse.statet.jcommons.lang.SystemUtils; |
| import org.eclipse.statet.jcommons.runtime.CommonsRuntime; |
| import org.eclipse.statet.jcommons.status.InfoStatus; |
| |
| import org.eclipse.statet.rj.eclient.graphics.RGraphics; |
| |
| |
| @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 @Nullable [] 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 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; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return this.swtFont.toString() + " size= " + this.size; |
| } |
| |
| } |
| |
| |
| private static final boolean EBUG_575614_WORKAROUND; // bug 575614 GTK (SWT crash in background thread) |
| private static final boolean EBUG_573065_WORKAROUND; // bug 573065 MacOS (SWT bug 577192) |
| static { |
| EBUG_575614_WORKAROUND= getEnabled("org.eclipse.statet.rj.eclient.graphics.Workaround575614.enabled", //$NON-NLS-1$ |
| () -> (SWT.getPlatform().equals("gtk")) ); //$NON-NLS-1$ |
| EBUG_573065_WORKAROUND= getEnabled("org.eclipse.statet.rj.eclient.graphics.Workaround573065.enabled", //$NON-NLS-1$ |
| () -> (SWT.getPlatform().equals("cocoa") //$NON-NLS-1$ |
| && SystemUtils.getLocalArch() != SystemUtils.ARCH_ARM_64 )); |
| } |
| |
| private static final class TestBed { |
| |
| |
| private GC gc; |
| |
| 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 TestBed(final Device device) { |
| // image size |
| try (final var managedFont= autoDispose(new Font(device, |
| new FontData(device.getSystemFont().getFontData()[0].getName(), HR_FONTSIZE, 0) ))) { |
| final GC gc= new GC(device); |
| try { |
| gc.setFont(managedFont.get()); |
| this.imageHeigth= (int)(gc.getFontMetrics().getHeight() * 1.5); |
| this.imageWidth= this.imageHeigth * 2; |
| } |
| finally { |
| gc.dispose(); |
| } |
| } |
| |
| this.image= new Image(device, this.imageWidth, this.imageHeigth); |
| |
| this.font= null; |
| |
| createGC(); |
| clearImage(); |
| final var imageData= 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]; |
| } |
| |
| private void createGC() { |
| assert (this.gc == null); |
| final var gc= new GC(this.image); |
| gc.setAdvanced(true); |
| gc.setAntialias(SWT.ON); |
| gc.setTextAntialias(SWT.ON); |
| gc.setInterpolation(SWT.HIGH); |
| gc.setAlpha(255); |
| gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WHITE)); |
| gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_BLACK)); |
| gc.setFont(null); |
| this.gc= gc; |
| } |
| |
| |
| public void resetGC() { |
| disposeGC(); |
| createGC(); |
| } |
| |
| public void setFont(final FontInstance font) { |
| // if (gcFont != font) { // E-Bug #319125 |
| this.font= font; |
| this.gc.setFont(font.swtFont); |
| // } |
| if (font.baseLine == 0) { |
| final FontMetrics fontMetrics= this.gc.getFontMetrics(); |
| font.baseLine= fontMetrics.getLeading() + fontMetrics.getAscent(); |
| font.metricTolerance= fontMetrics.getAscent() * 0.05; |
| |
| font.swtFontProperties= new double[] { |
| font.baseLine, |
| }; |
| this.gc.setFont(font.swtFont); // E-Bug #319125 |
| } |
| } |
| |
| public FontInstance getFont() { |
| return this.font; |
| } |
| |
| public int getStringWidth(final String txt) { |
| return this.gc.stringExtent(txt).x; |
| } |
| |
| public void clearImage() { |
| this.gc.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.gc.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; |
| } |
| |
| |
| private void disposeGC() { |
| final GC gc= this.gc; |
| if (gc != null) { |
| this.gc= null; |
| gc.dispose(); |
| } |
| } |
| |
| public void dispose() { |
| disposeGC(); |
| 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()) { |
| if (font.charMetrics.length == 0) { // init required |
| execImageRunnable(() -> { |
| final TestBed test= getTestBed(true); |
| test.setFont(font); |
| |
| // init |
| font.charMetrics= new double[Math.max(512 * METRIC_COUNT, |
| chIdx + 128 * METRIC_COUNT)]; |
| |
| font.charMetrics[' ' * METRIC_COUNT + METRIC_IDX_ADVWIDTH] = |
| (test.gc.getAdvanceWidth('m') * 0.2 + test.gc.getAdvanceWidth(' ') * 1.0) / 2.0; |
| font.charMetrics[' ' * METRIC_COUNT + METRIC_IDX_ASCENT]= 0; |
| font.charMetrics[' ' * METRIC_COUNT + METRIC_IDX_DESCENT]= 0; |
| |
| font.ascentUp= checkCharAscentMean(test, new char[] { 'A', 'M', 'O', 'E' }); |
| font.ascentLow= checkCharAscentMean(test, 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; |
| |
| // check ch |
| if (font.charMetrics[chIdx] == 0) { |
| computeCharHeights(test, ch, chIdx); |
| } |
| }); |
| } |
| 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; |
| } |
| |
| // check ch |
| if (font.charMetrics[chIdx] == 0) { |
| execImageRunnable(() -> { |
| final TestBed test= getTestBed(true); |
| test.setFont(font); |
| |
| computeCharHeights(test, 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 TestBed test, final int ch, final int idx) { |
| final FontInstance font= test.getFont(); |
| font.charMetrics[idx + METRIC_IDX_ADVWIDTH] = |
| test.gc.getAdvanceWidth((char) ch); |
| |
| final String s= String.valueOf((char) ch); |
| |
| test.clearImage(); |
| test.drawText(s); |
| final int firstLine= test.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= test.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]= test.getStringWidth(s); |
| } |
| } |
| |
| private double checkCharAscentMean(final TestBed 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.gc.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 TestBed test= getTestBed(false); |
| test.setFont(font); |
| |
| final double width= test.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! |
| var fontProperties= font.swtFontProperties; |
| if (fontProperties == null) { |
| synchronized (getTestLock()) { |
| final TestBed test= getTestBed(false); |
| test.setFont(font); |
| } |
| fontProperties= font.swtFontProperties; |
| } |
| return fontProperties; |
| } |
| |
| 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; |
| } |
| } |
| } |
| } |
| } |
| |
| |
| @Override |
| public String toString() { |
| return "FontFamily " + this.name; //$NON-NLS-1$ |
| } |
| |
| } |
| |
| |
| private final Display display; |
| |
| private final Object testBedLock= new Object(); |
| private @Nullable TestBed testBed; |
| private boolean disposed; |
| |
| private final Map<String, FontFamily> fontFamilies= new HashMap<>(); |
| |
| |
| public FontManager(final Display display) { |
| this.display= display; |
| CommonsRuntime.log(new InfoStatus(RGraphics.BUNDLE_ID, |
| "FontManager Config: " |
| + "bug575614= " + EBUG_575614_WORKAROUND + ", " |
| + "bug573065= " + EBUG_573065_WORKAROUND )); |
| } |
| |
| |
| 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; |
| } |
| |
| |
| private Object getTestLock() { |
| // if (Thread.currentThread() == this.display.getThread()) { |
| // System.out.println("FIXME risk of dead lock"); |
| // } |
| return this.testBedLock; |
| } |
| |
| private TestBed getTestBed(final boolean bug573065) { |
| var test= this.testBed; |
| if (test == null) { |
| test= this.display.syncCall( |
| () -> (!this.disposed) ? new TestBed(this.display) : null ); |
| if (test == null) { |
| throw new RuntimeException("disposed"); |
| } |
| this.testBed= test; |
| } |
| if (EBUG_573065_WORKAROUND && bug573065) { |
| test.resetGC(); |
| } |
| return test; |
| } |
| |
| private void execImageRunnable(final Runnable runnable) { |
| if (EBUG_575614_WORKAROUND || EBUG_573065_WORKAROUND) { |
| this.display.syncExec(runnable); |
| } |
| else { |
| runnable.run(); |
| } |
| } |
| |
| |
| public void dispose() { |
| this.disposed= true; |
| for (final FontFamily fontFamily : this.fontFamilies.values()) { |
| fontFamily.dispose(); |
| } |
| this.fontFamilies.clear(); |
| |
| final var test= this.testBed; |
| if (test != null) { |
| this.testBed= null; |
| test.dispose(); |
| } |
| } |
| |
| |
| private static boolean getEnabled(final String key, final BooleanSupplier detect) { |
| final String value= System.getProperty(key); |
| if (value != null && !value.isEmpty()) { |
| return Boolean.parseBoolean(value); |
| } |
| return detect.getAsBoolean(); |
| } |
| |
| } |