Bug 516293: [hidpi][cocoa] Blurry line number ruler on retina display
diff --git a/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/revisions/RevisionPainter.java b/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/revisions/RevisionPainter.java
index 34cdcaa..50a257a 100644
--- a/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/revisions/RevisionPainter.java
+++ b/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/revisions/RevisionPainter.java
@@ -588,6 +588,11 @@
 	 * @since 3.3
 	 */
 	private int fLastWidth= -1;
+	/**
+	 * The zoom level for the current painting operation. Workaround for bug 516293.
+	 * @since 3.12
+	 */
+	private int fZoom= 100;
 
 	/**
 	 * Creates a new revision painter for a vertical ruler column.
@@ -658,6 +663,20 @@
 	}
 
 	/**
+	 * Sets the zoom level for the current painting operation. Workaround for bug 516293.
+	 * 
+	 * @param zoom the zoom to set
+	 * @since 3.12
+	 */
+	public void setZoom(int zoom) {
+		fZoom= zoom;
+	}
+
+	private int autoScaleUp(int value) {
+		return value * fZoom / 100;
+	}
+
+	/**
 	 * Delegates the painting of the quick diff colors to this painter. The painter will draw the
 	 * color boxes onto the passed {@link GC} for all model (document) lines in
 	 * <code>visibleModelLines</code>.
@@ -1058,7 +1077,7 @@
 		int y1= fWidget.getLinePixel(range.getStartLine());
 		int y2= fWidget.getLinePixel(range.getStartLine() + range.getNumberOfLines());
 
-		return new Rectangle(0, y1, getWidth(), y2 - y1 - 1);
+		return new Rectangle(0, autoScaleUp(y1), autoScaleUp(getWidth()), autoScaleUp(y2 - y1 - 1));
 	}
 
 	/**
diff --git a/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/source/DiffPainter.java b/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/source/DiffPainter.java
index 53c82fe..33a6fea 100644
--- a/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/source/DiffPainter.java
+++ b/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/source/DiffPainter.java
@@ -82,6 +82,11 @@
 	private final AnnotationListener fAnnotationListener= new AnnotationListener();
 	/** The shared color provider, possibly <code>null</code>. */
 	private final ISharedTextColors fSharedColors;
+	/**
+	 * The zoom level for the current painting operation. Workaround for bug 516293.
+	 * @since 3.12
+	 */
+	private int fZoom= 100;
 
 	/**
 	 * Creates a new diff painter for a vertical ruler column.
@@ -135,6 +140,20 @@
 	}
 
 	/**
+	 * Sets the zoom level for the current painting operation. Workaround for bug 516293.
+	 * 
+	 * @param zoom the zoom to set
+	 * @since 3.12
+	 */
+	public void setZoom(int zoom) {
+		fZoom= zoom;
+	}
+
+	private int autoScaleUp(int value) {
+		return value * fZoom / 100;
+	}
+
+	/**
 	 * Delegates the painting of the quick diff colors to this painter. The painter will draw the
 	 * color boxes onto the passed {@link GC} for all model (document) lines in
 	 * <code>visibleModelLines</code>.
@@ -226,7 +245,7 @@
 			// draw background color if special
 			if (hasSpecialColor(info)) {
 				gc.setBackground(getColor(info));
-				gc.fillRectangle(0, y, width, lineHeight);
+				gc.fillRectangle(0, autoScaleUp(y), autoScaleUp(width), autoScaleUp(lineHeight));
 			}
 
 			/* Deletion Indicator: Simply a horizontal line */
@@ -234,10 +253,11 @@
 			int delBelow= info.getRemovedLinesBelow();
 			if (delBefore > 0 || delBelow > 0) {
 				gc.setForeground(deletionColor);
+				gc.setLineWidth(autoScaleUp(1));
 				if (delBefore > 0)
-					gc.drawLine(0, y, width, y);
+					gc.drawLine(0, autoScaleUp(y), autoScaleUp(width), autoScaleUp(y));
 				if (delBelow > 0)
-					gc.drawLine(0, y + lineHeight - 1, width, y + lineHeight - 1);
+					gc.drawLine(0, autoScaleUp(y + lineHeight - 1), autoScaleUp(width), autoScaleUp(y + lineHeight - 1));
 			}
 		}
 	}
diff --git a/org.eclipse.jface.text/src/org/eclipse/jface/text/source/LineNumberChangeRulerColumn.java b/org.eclipse.jface.text/src/org/eclipse/jface/text/source/LineNumberChangeRulerColumn.java
index 5a41b5d..a835305 100644
--- a/org.eclipse.jface.text/src/org/eclipse/jface/text/source/LineNumberChangeRulerColumn.java
+++ b/org.eclipse.jface.text/src/org/eclipse/jface/text/source/LineNumberChangeRulerColumn.java
@@ -166,10 +166,13 @@
 	void doPaint(GC gc, ILineRange visibleLines) {
 		Color foreground= gc.getForeground();
 		if (visibleLines != null) {
-			if (fRevisionPainter.hasInformation())
+			if (fRevisionPainter.hasInformation()) {
+				fRevisionPainter.setZoom(fZoom);
 				fRevisionPainter.paint(gc, visibleLines);
-			else if (fDiffPainter.hasInformation()) // don't paint quick diff colors if revisions are painted
+			} else if (fDiffPainter.hasInformation()) { // don't paint quick diff colors if revisions are painted
+				fDiffPainter.setZoom(fZoom);
 				fDiffPainter.paint(gc, visibleLines);
+			}
 		}
 		gc.setForeground(foreground);
 		if (fShowNumbers || fCharacterDisplay)
diff --git a/org.eclipse.jface.text/src/org/eclipse/jface/text/source/LineNumberRulerColumn.java b/org.eclipse.jface.text/src/org/eclipse/jface/text/source/LineNumberRulerColumn.java
index 00b895e..8100216 100644
--- a/org.eclipse.jface.text/src/org/eclipse/jface/text/source/LineNumberRulerColumn.java
+++ b/org.eclipse.jface.text/src/org/eclipse/jface/text/source/LineNumberRulerColumn.java
@@ -14,6 +14,7 @@
  *******************************************************************************/
 package org.eclipse.jface.text.source;
 
+import java.lang.ref.WeakReference;
 import java.util.Arrays;
 
 import org.eclipse.swt.SWT;
@@ -30,9 +31,12 @@
 import org.eclipse.swt.events.PaintListener;
 import org.eclipse.swt.graphics.Color;
 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.graphics.ImageDataProvider;
 import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.graphics.Rectangle;
 import org.eclipse.swt.widgets.Canvas;
@@ -41,6 +45,8 @@
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.TypedListener;
 
+import org.eclipse.jface.util.Util;
+
 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IRegion;
@@ -62,6 +68,17 @@
 public class LineNumberRulerColumn implements IVerticalRulerColumn {
 
 	/**
+	 * <code>true</code> if we're on a Mac, where drawing on an Image currently only draws at 100% zoom level,
+	 * which results in blurry line numbers on a Retina display.
+	 *
+	 * @see <a href="https://bugs.eclipse.org/516293">bug 516293</a>
+	 * @since 3.6
+	 */
+	private final boolean IS_MAC_BUG_516293= Util.isMac()
+			&& !"false".equals(System.getProperty("LineNumberRulerColumn.retina.workaround")) //$NON-NLS-1$ //$NON-NLS-2$
+			&& internalSupportsZoomedPaint();
+
+	/**
 	 * Internal listener class.
 	 */
 	class InternalListener implements IViewportListener, ITextListener {
@@ -420,6 +437,13 @@
 	};
 	/* @since 3.2 */
 	private MouseHandler fMouseHandler;
+	/*
+	 * Zoom level and cached font for the current painting operation. Workaround for bug 516293.
+	 * @since 3.12
+	 */
+	int fZoom= 100;
+	private WeakReference<Font> fLastFont;
+	private Font fLastZoomedFont;
 
 
 	/**
@@ -666,6 +690,12 @@
 			fBuffer.dispose();
 			fBuffer= null;
 		}
+
+		if (fLastZoomedFont != null) {
+			fLastZoomedFont.dispose();
+			fLastZoomedFont= null;
+			fLastFont= null;
+		}
 	}
 
 	/**
@@ -687,31 +717,110 @@
 				fBuffer= null;
 			}
 		}
-		if (fBuffer == null)
-			fBuffer= new Image(fCanvas.getDisplay(), size.x, size.y);
 
-		GC gc= new GC(fBuffer);
-		gc.setFont(fCanvas.getFont());
-		if (fForeground != null)
-			gc.setForeground(fForeground);
+		ILineRange visibleLines= JFaceTextUtil.getVisibleModelLines(fCachedTextViewer);
+		if (visibleLines == null)
+			return;
 
-		try {
-			gc.setBackground(getBackground(fCanvas.getDisplay()));
-			gc.fillRectangle(0, 0, size.x, size.y);
+		if (IS_MAC_BUG_516293) {
+			/* FIXME: Workaround (bug 516293):
+			 * Relies on SWT implementation detail that GC drawing on macOS only draws at 100% zoom level.
+			 * For higher zoom levels (200%), we manually scale the font and drawing coordinates,
+			 * and then use getImageData(100) to extract the high-resolution image data. */
+			fBuffer= new Image(fCanvas.getDisplay(), (ImageDataProvider) zoom -> {
+				fZoom = zoom;
+				internalSetZoom(zoom);
+				int width= size.x * zoom / 100;
+				int height= size.y * zoom / 100;
+				Image gcImage= new Image(fCanvas.getDisplay(), width, height);
 
-			ILineRange visibleLines= JFaceTextUtil.getVisibleModelLines(fCachedTextViewer);
-			if (visibleLines == null)
-				return;
-			fScrollPos= fCachedTextWidget.getTopPixel();
-			doPaint(gc, visibleLines);
-		} finally {
-			gc.dispose();
+				GC gc= new GC(gcImage);
+				Font font= fCanvas.getFont();
+				if (zoom != 100) {
+					if (fLastFont != null && font == fLastFont.get()) {
+						font= fLastZoomedFont;
+					} else {
+						fLastFont= new WeakReference<>(font);
+						FontData fontData= font.getFontData()[0];
+						fontData.setHeight(fontData.getHeight() * zoom / 100);
+						font= new Font(font.getDevice(), fontData);
+						fLastZoomedFont= font;
+					}
+				}
+				gc.setFont(font);
+				if (fForeground != null)
+					gc.setForeground(fForeground);
+
+				try {
+					gc.setBackground(getBackground(fCanvas.getDisplay()));
+					gc.fillRectangle(0, 0, width, height);
+
+					fScrollPos= fCachedTextWidget.getTopPixel();
+					doPaint(gc, visibleLines);
+				} finally {
+					gc.dispose();
+					fZoom= 100;
+				}
+
+				ImageData imageData= gcImage.getImageData(100);
+				gcImage.dispose();
+				return imageData;
+			});
+			
+		} else {
+			if (fBuffer == null)
+				fBuffer= new Image(fCanvas.getDisplay(), size.x, size.y);
+
+			GC gc= new GC(fBuffer);
+			gc.setFont(fCanvas.getFont());
+			if (fForeground != null)
+				gc.setForeground(fForeground);
+
+			try {
+				gc.setBackground(getBackground(fCanvas.getDisplay()));
+				gc.fillRectangle(0, 0, size.x, size.y);
+
+				fScrollPos= fCachedTextWidget.getTopPixel();
+				doPaint(gc, visibleLines);
+			} finally {
+				gc.dispose();
+			}
 		}
 
 		dest.drawImage(fBuffer, 0, 0);
 	}
 
 	/**
+	 * This method is not API and it is expected to disappear in Eclipse 4.8.
+	 * Subclasses that want to take advantage of the unsupported workaround for bug 516258
+	 * can re-implement this method and return true.
+	 * 
+	 * @return true iff this class supports the workaround for bug 516258
+	 * 
+	 * @nooverride This method is not intended to be re-implemented or extended by clients.
+	 * @noreference This method is not intended to be referenced by clients.
+	 * @since 3.12
+	 */
+	protected boolean internalSupportsZoomedPaint() {
+		return getClass().getPackage().equals(LineNumberChangeRulerColumn.class.getPackage());
+	}
+
+	/**
+	 * This method is not API and it is expected to disappear in Eclipse 4.8.
+	 * Subclasses that want to take advantage of the unsupported workaround for bug 516258
+	 * can override this method and store the given zoom level for later use.
+	 * 
+	 * @param zoom the zoom level to use for drawing operations
+	 * 
+	 * @nooverride This method is not intended to be re-implemented or extended by clients.
+	 * @noreference This method is not intended to be referenced by clients.
+	 * @since 3.12
+	 */
+	protected void internalSetZoom(int zoom) {
+		// callback for subclasses
+	}
+	
+	/**
 	 * Returns the view port height in lines.
 	 *
 	 * @return the view port height in lines
@@ -848,7 +957,7 @@
 		}
 		int indentation= fIndentation[index];
 		int baselineBias= getBaselineBias(gc, widgetLine);
-		gc.drawString(s, indentation, y + baselineBias, true);
+		gc.drawString(s, indentation * fZoom / 100, (y + baselineBias) * fZoom / 100, true);
 	}
 
 	/**