blob: f7952b3092e598bed0eea73243fed25fb39b3167 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2011, 2019 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;
}
}
}