blob: 714613b2650ef693280c718f9b8aa7c90bcceff2 [file] [log] [blame]
/*=============================================================================#
# 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();
}
}