diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java index 47b20458042..d6af2d2c10b 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java @@ -18,6 +18,7 @@ import java.util.*; import java.util.concurrent.*; +import java.util.function.*; import java.util.stream.*; import org.eclipse.swt.*; @@ -146,6 +147,184 @@ private static int renderTextAndCountNonWhitePixels(Image target, Font font, Str return count; } + /** + * U+FFFE is a Unicode non-character that no standard font has a glyph for. + * Appending it to a string makes an advanced GC lay that string out with + * GDI+ instead of letting GDI compute the glyph positions, which is the only + * way to exercise the GDI+ tab stop handling. + */ + private static final String UNSUPPORTED_GLYPH = String.valueOf((char) 0xFFFE); + + /** + * Fonts covering both ends of the space-width/average-character-width ratio: + * in proportional fonts a space is roughly half the average character width, + * while in monospace fonts the two nearly coincide. A tab stop derived from + * the space width therefore only misbehaves noticeably for the proportional + * ones, so both kinds have to be covered. + */ + private static Stream tabStopTestFonts() { + return Stream.of("Segoe UI", "Arial", "Times New Roman", "Courier New", "Consolas"); + } + + /** + * The extents of the GDI and the GDI+ path are each rounded up to whole + * pixels independently, so measurements derived from a difference of two + * extents may legitimately be off by one pixel. + */ + private static final int ROUNDING_TOLERANCE = 1; + + /** + * Verifies that a tab is expanded to eight times the font's average + * character width, which is the convention Win32's own {@code DrawText()} + * and {@code TabbedTextOut()} follow: {@code TabbedTextOut()} is documented + * to expand tabs to "eight times the average character width" by default, + * and {@code DRAWTEXTPARAMS.iTabLength} is documented to be measured "in + * units equal to the average character width". + *

+ * This pins down the constant the GDI+ path is expected to reproduce. + */ + @ParameterizedTest + @MethodSource("tabStopTestFonts") + public void tabStopWidthEqualsEightAverageCharacterWidths(String fontName) { + Display display = Display.getDefault(); + Image image = new Image(display, 400, 100); + Font font = new Font(display, fontName, 12, SWT.NORMAL); + GC gc = new GC(image); + try { + gc.setFont(font); + int averageCharacterWidth = gc.getFontMetrics().handle.tmAveCharWidth; + assertWithinRoundingTolerance(8 * averageCharacterWidth, measureTabStopWidth(gc), + "a tab must be expanded to eight average character widths for font " + fontName); + } finally { + gc.dispose(); + font.dispose(); + image.dispose(); + } + } + + /** + * Verifies that tab stops are expanded to the same width no matter whether + * text is rendered via plain GDI or via GDI+. + *

+ * Both paths must use eight times the font's average character width + * ({@code TEXTMETRIC.tmAveCharWidth}). The GDI+ path used to derive its tab + * stop width from the width of a single space glyph instead. That is a + * different metric, not merely a differently computed one: in proportional + * fonts a space is roughly half the average character width, so tab stops + * came out about half as wide whenever that path was taken. + */ + @ParameterizedTest + @MethodSource("tabStopTestFonts") + public void tabStopWidthIsConsistentBetweenGdiAndGdipRendering(String fontName) { + Display display = Display.getDefault(); + Image image = new Image(display, 400, 100); + Font font = new Font(display, fontName, 12, SWT.NORMAL); + try { + int gdiTabStopWidth = withGC(image, font, false, GCWin32Tests::measureTabStopWidth); + int gdipTabStopWidth = withGC(image, font, true, GCWin32Tests::measureTabStopWidth); + assertWithinRoundingTolerance(gdiTabStopWidth, gdipTabStopWidth, + "GDI+ rendering must expand a tab to the same width as GDI rendering for font " + fontName); + } finally { + font.dispose(); + image.dispose(); + } + } + + /** + * Verifies that consecutive tabs advance to consecutive tab stops instead of + * collapsing into a single one, i.e. that the tab stop width repeats. GDI + * derives the repetition from its own {@code (position / width + 1) * width} + * calculation, whereas GDI+ gets a single tab stop distance passed to + * {@code StringFormat::SetTabStops} and repeats it internally; this asserts + * that both arrive at the same layout. + */ + @ParameterizedTest + @MethodSource("tabStopTestFonts") + public void consecutiveTabsAdvanceByWholeTabStops(String fontName) { + Display display = Display.getDefault(); + Image image = new Image(display, 800, 100); + Font font = new Font(display, fontName, 12, SWT.NORMAL); + try { + for (boolean advanced : new boolean[] { false, true }) { + int twoTabStops = withGC(image, font, advanced, + gc -> measureTabAdvance(gc, "\t\t") ); + int oneTabStop = withGC(image, font, advanced, GCWin32Tests::measureTabStopWidth); + assertWithinRoundingTolerance(2 * oneTabStop, twoTabStops, + "two tabs must advance by two tab stops for font " + fontName + + " (advanced=" + advanced + ")"); + } + } finally { + font.dispose(); + image.dispose(); + } + } + + /** + * Verifies that a tab advances to the next tab stop rather than adding a + * fixed amount of space, so that text following a tab starts at the same + * column regardless of what precedes the tab within the same tab stop. + */ + @ParameterizedTest + @MethodSource("tabStopTestFonts") + public void textAfterTabStartsAtSameTabStopRegardlessOfPrecedingText(String fontName) { + Display display = Display.getDefault(); + Image image = new Image(display, 400, 100); + Font font = new Font(display, fontName, 12, SWT.NORMAL); + try { + for (boolean advanced : new boolean[] { false, true }) { + int withoutPrefix = withGC(image, font, advanced, gc -> measureExtent(gc, "\tB")); + int withPrefix = withGC(image, font, advanced, gc -> measureExtent(gc, "A\tB")); + assertWithinRoundingTolerance(withoutPrefix, withPrefix, + "text following a tab must start at the same tab stop no matter what precedes the tab, " + + "for font " + fontName + " (advanced=" + advanced + ")"); + } + } finally { + font.dispose(); + image.dispose(); + } + } + + /** + * Returns the width of a single tab stop, measured as the advance a leading + * tab adds. A leading tab always expands to exactly one tab stop, so unlike + * a tab in the middle of a string this measurement is not diluted by the + * slightly different glyph advances of the GDI and the GDI+ text layout + * engine. + */ + private static int measureTabStopWidth(GC gc) { + return measureTabAdvance(gc, "\t"); + } + + /** + * Returns the horizontal advance the given leading tabs add to the extent of + * the text that follows them. + */ + private static int measureTabAdvance(GC gc, String leadingTabs) { + return measureExtent(gc, leadingTabs + "B") - measureExtent(gc, "B"); + } + + private static int measureExtent(GC gc, String text) { + // measure in pixels to keep the comparison free of the point/pixel + // conversion the public API applies at non-100% zoom levels + return gc.textExtentInPixels(text + UNSUPPORTED_GLYPH, SWT.DRAW_TAB).x; + } + + private static void assertWithinRoundingTolerance(int expected, int actual, String message) { + assertTrue(Math.abs(actual - expected) <= ROUNDING_TOLERANCE, + message + " (expected " + expected + ", was " + actual + ")"); + } + + private static int withGC(Image target, Font font, boolean advanced, ToIntFunction measurement) { + GC gc = new GC(target); + try { + gc.setFont(font); + gc.setAdvanced(advanced); + return measurement.applyAsInt(gc); + } finally { + gc.dispose(); + } + } + /** * Regression test for the size calculation in scaling/cropping GC.drawImage() * operations with asymmetric source dimensions (smaller height than width) at diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java index 525cf88296d..cb4246c32d8 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java @@ -2886,7 +2886,7 @@ void drawText(long gdipGraphics, String string, int x, int y, int flags, Point s if (hFont != 0) OS.SelectObject(hdc, oldFont); Gdip.Graphics_ReleaseHDC(gdipGraphics, hdc); if (gdip) { - drawTextGDIP(gdipGraphics, string, x, y, flags, size == null, size); + drawTextGDIP(gdipGraphics, string, x, y, flags, size == null, size, lptm); return; } int i = 0, start = 0, end = 0, drawX = x, drawY = y, width = 0, mnemonicIndex = -1; @@ -3056,7 +3056,7 @@ private RectF drawText(long gdipGraphics, char[] buffer, int start, int length, return bounds; } -private void drawTextGDIP(long gdipGraphics, String string, int x, int y, int flags, boolean draw, Point size) { +private void drawTextGDIP(long gdipGraphics, String string, int x, int y, int flags, boolean draw, Point size, TEXTMETRIC lptm) { boolean needsBounds = !draw || (flags & SWT.DRAW_TRANSPARENT) == 0; char[] buffer; if ((flags & SWT.DRAW_DELIMITER) == 0) { @@ -3078,7 +3078,15 @@ private void drawTextGDIP(long gdipGraphics, String string, int x, int y, int fl int formatFlags = Gdip.StringFormat_GetFormatFlags(format) | Gdip.StringFormatFlagsMeasureTrailingSpaces; if ((data.style & SWT.MIRRORED) != 0) formatFlags |= Gdip.StringFormatFlagsDirectionRightToLeft; Gdip.StringFormat_SetFormatFlags(format, formatFlags); - float[] tabs = (flags & SWT.DRAW_TAB) != 0 ? new float[]{measureSpace(data.gdipFont, format) * 8} : new float[1]; + // Use the same tab stop width as the GDI-based text rendering path: 8 * the + // font's average character width, which is what Win32's own DrawText() and + // TabbedTextOut() use by default. This used to be 8 * the width of a single + // space glyph, which is a different metric rather than a differently + // computed one: in proportional fonts a space is roughly half the average + // character width, so tab stops came out about half as wide whenever this + // path was taken. (In monospace fonts the two nearly coincide, which is why + // the discrepancy was easy to miss.) + float[] tabs = (flags & SWT.DRAW_TAB) != 0 ? new float[]{lptm.tmAveCharWidth * 8} : new float[1]; Gdip.StringFormat_SetTabStops(format, 0, tabs.length, tabs); int hotkeyPrefix = (flags & SWT.DRAW_MNEMONIC) != 0 ? Gdip.HotkeyPrefixShow : Gdip.HotkeyPrefixNone; if ((flags & SWT.DRAW_MNEMONIC) != 0 && (data.uiState & OS.UISF_HIDEACCEL) != 0) hotkeyPrefix = Gdip.HotkeyPrefixHide; @@ -4658,13 +4666,6 @@ public boolean isDisposed() { return handle == 0; } -private float measureSpace(long font, long format) { - PointF pt = new PointF(); - RectF bounds = new RectF(); - Gdip.Graphics_MeasureString(data.gdipGraphics, new char[]{' '}, 1, font, pt, format, bounds); - return bounds.Width; -} - /** * Sets the receiver to always use the operating system's advanced graphics * subsystem for all graphics operations if the argument is true. diff --git a/examples/org.eclipse.swt.snippets/.classpath_cocoa b/examples/org.eclipse.swt.snippets/.classpath_cocoa index bcad73af751..6f6d5d56841 100644 --- a/examples/org.eclipse.swt.snippets/.classpath_cocoa +++ b/examples/org.eclipse.swt.snippets/.classpath_cocoa @@ -2,6 +2,6 @@ - + diff --git a/examples/org.eclipse.swt.snippets/.classpath_gtk b/examples/org.eclipse.swt.snippets/.classpath_gtk index bcad73af751..6f6d5d56841 100644 --- a/examples/org.eclipse.swt.snippets/.classpath_gtk +++ b/examples/org.eclipse.swt.snippets/.classpath_gtk @@ -2,6 +2,6 @@ - + diff --git a/examples/org.eclipse.swt.snippets/Snippets.md b/examples/org.eclipse.swt.snippets/Snippets.md index 08cd32f387c..9f3e1becfdd 100644 --- a/examples/org.eclipse.swt.snippets/Snippets.md +++ b/examples/org.eclipse.swt.snippets/Snippets.md @@ -198,6 +198,7 @@ To contribute a new snippet, [create a snippet contribution as a pull request](h - [draw 2 polylines with different line attributes](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet252.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet252.png "Preview for Snippet 252") - [draw lines with configurable line width, scaling and rotation](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet381.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet381.png "Preview for Snippet 381") - [crop and scale images via source and destination values](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet389.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet389.png "Preview for Snippet 389") +- [render different types of text with GDI vs. GDI+ (Windows-only)](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet395.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet395.png "Preview for Snippet 395") ### **Gesture, Touch support** - [create a shell and listen for TouchEvents](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet352.java) diff --git a/examples/org.eclipse.swt.snippets/previews/Snippet395.png b/examples/org.eclipse.swt.snippets/previews/Snippet395.png new file mode 100644 index 00000000000..b802778b031 Binary files /dev/null and b/examples/org.eclipse.swt.snippets/previews/Snippet395.png differ diff --git a/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet395.java b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet395.java new file mode 100644 index 00000000000..953ca26a583 --- /dev/null +++ b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet395.java @@ -0,0 +1,224 @@ +/******************************************************************************* + * Copyright (c) 2026 Vector Informatik GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.snippets; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.eclipse.swt.*; +import org.eclipse.swt.custom.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.layout.*; +import org.eclipse.swt.widgets.*; + +/* + * Windows plain GDI vs. GDI+ text rendering snippet. + * + * On Windows, GC.drawText() renders text in one of two ways: with plain GDI + * (OS.DrawText) whenever the GC is not advanced, and with GDI+ once + * GC.setAdvanced(true) is active. The two engines compute text layout + * independently, so kerning, tab stop width, mnemonic underlining and + * bidi/mirroring can all come out differently depending on which one draws. + * + * This snippet renders a series of text properties, one row per property, and + * lets the rendering path be switched at runtime, so that the results can be + * compared visually without restarting the process: + * - "Use GDI+ (advanced) rendering" calls GC.setAdvanced() and re-renders + * every row, switching between plain GDI and GDI+. + * + * The rows labelled "unsupported glyph (U+FFFE)" append U+FFFE, a Unicode + * non-character that no standard font has a glyph for. Strings containing such + * a character are always laid out by GDI+ itself rather than having their glyph + * positions computed by GDI, which is what makes the GDI+ tab stop and + * decoration handling observable. The trailing box-shaped glyph drawn for + * U+FFFE itself is expected. + * + * What to expect while toggling: + * - The two tab rows must expand tabs to the same column width in every + * combination. + * - "Mnemonic" shows an underlined "F" in every combination (it does not + * depend on font-level decoration). + * - "Kerning pair", the tab rows and "Mirrored / RTL" may differ slightly in + * spacing/positioning between the engines, but should never render blank, + * wildly stretched/compressed, or with overlapping glyphs. + * - The script/charset rows (Arabic, Hebrew, CJK, Cyrillic, Greek, combining + * diacritics) should render recognisable, visible glyphs in every + * combination. + * - "Underlined"/"Strikeout"/"Bold + underlined" render their decoration with + * plain GDI, but go blank once GDI+/advanced is enabled, unless U+FFFE + * forces GDI+'s own text layout. See + * https://github.com/eclipse-platform/eclipse.platform.swt/issues/3091 . + * + * On platforms other than Windows, GC.setAdvanced() does not select a + * different text rendering engine, so all rows render identically regardless + * of the checkbox state. + * + * For a list of all SWT example snippets see + * http://www.eclipse.org/swt/snippets/ + */ +public class Snippet395 { + + /** One row of the comparison: a label, the text properties to apply, and how to draw it. */ + record TextRow(String label, int fontStyle, boolean underline, boolean strikeout, String text, int drawFlags, + int gcStyle) { + TextRow(String label, String text) { + this(label, SWT.NORMAL, false, false, text, SWT.DRAW_TRANSPARENT, SWT.NONE); + } + } + + /** + * Renders one fresh sample {@link Image} per row, either with plain GDI + * ({@code advanced == false}) or with GDI+ ({@code advanced == true}). + * Callers are responsible for disposing the previous set of images + * returned by an earlier call. + */ + private static Map renderSamples(Display display, java.util.List rows, + Map fonts, int sampleWidth, int sampleHeight, boolean advanced) { + Map samples = new HashMap<>(); + for (TextRow row : rows) { + Image sample = new Image(display, sampleWidth, sampleHeight); + GC gc = new GC(sample, row.gcStyle()); + try { + gc.setAdvanced(advanced); + gc.setBackground(new Color(255, 255, 255)); + gc.fillRectangle(sample.getBounds()); + gc.setForeground(new Color(0, 0, 0)); + gc.setFont(fonts.get(row)); + gc.drawText(row.text(), 5, 5, row.drawFlags()); + } finally { + gc.dispose(); + } + samples.put(row, sample); + } + return samples; + } + + @SuppressWarnings("restriction") + public static void main(String[] args) { + // U+FFFE has no glyph in any standard font. Appending it to a string makes + // GC.drawText() lay out that whole string with GDI+ instead of computing + // the glyph positions with GDI, so the rows using it always show what + // GDI+'s own text layout does with the respective text property. + String unsupportedGlyph = String.valueOf((char) 0xFFFE); + + java.util.List rows = new ArrayList<>(); + rows.add(new TextRow("Plain text", "Hello World")); + rows.add(new TextRow("Bold", SWT.BOLD, false, false, "Hello World", SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Italic", SWT.ITALIC, false, false, "Hello World", SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Underlined", SWT.NORMAL, true, false, "Hello World", SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Strikeout", SWT.NORMAL, false, true, "Hello World", SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Bold + underlined", SWT.BOLD, true, false, "Hello World", SWT.DRAW_TRANSPARENT, + SWT.NONE)); + rows.add(new TextRow("Underlined + unsupported glyph (U+FFFE)", SWT.NORMAL, true, false, + "Hi" + unsupportedGlyph, SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Mnemonic (accelerator underline)", SWT.NORMAL, false, false, "&File", + SWT.DRAW_MNEMONIC | SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Tab-separated columns", SWT.NORMAL, false, false, "A\tB\tC", SWT.DRAW_TAB, + SWT.NONE)); + rows.add(new TextRow("Tab-separated columns + unsupported glyph (U+FFFE)", SWT.NORMAL, false, false, + "A\tB\tC" + unsupportedGlyph, SWT.DRAW_TAB, SWT.NONE)); + rows.add(new TextRow("Kerning pair", "AVATAR WAVE To Yes")); + rows.add(new TextRow("Mirrored / RTL", SWT.NORMAL, false, false, "Hello World", SWT.DRAW_TRANSPARENT, + SWT.RIGHT_TO_LEFT)); + rows.add(new TextRow("Arabic", "\u0645\u0631\u062d\u0628\u0627 \u0628\u0627\u0644\u0639\u0627\u0644\u0645")); + rows.add(new TextRow("Hebrew", "\u05e9\u05dc\u05d5\u05dd \u05e2\u05d5\u05dc\u05dd")); + rows.add(new TextRow("CJK (Chinese)", "\u4f60\u597d\u4e16\u754c")); + rows.add(new TextRow("Cyrillic", "\u041f\u0440\u0438\u0432\u0435\u0442 \u043c\u0438\u0440")); + rows.add(new TextRow("Greek", "\u0393\u03b5\u03b9\u03ac \u03c3\u03bf\u03c5 \u039a\u03cc\u03c3\u03bc\u03b5")); + rows.add(new TextRow("Combining diacritics", "e\u0301clat na\u0308\u0131ve")); + + Display display = new Display(); + Shell shell = new Shell(display); + shell.setLayout(new GridLayout()); + shell.setText("Text rendering comparison"); + + Label info = new Label(shell, SWT.WRAP); + info.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + Button advancedCheckbox = new Button(shell, SWT.CHECK | SWT.WRAP); + advancedCheckbox.setText("Use GDI+ (advanced) rendering - GC.setAdvanced(true); " + + "uncheck to compare against plain GDI (GC.setAdvanced(false))"); + advancedCheckbox.setSelection(true); + advancedCheckbox.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + ScrolledComposite scroller = new ScrolledComposite(shell, SWT.V_SCROLL | SWT.BORDER); + scroller.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + scroller.setExpandHorizontal(true); + scroller.setExpandVertical(true); + + int rowHeight = 42; + int labelWidth = 260; + int sampleWidth = 320; + int sampleHeight = rowHeight - 4; + + Canvas canvas = new Canvas(scroller, SWT.NONE); + canvas.setSize(labelWidth + sampleWidth + 20, rows.size() * rowHeight + 10); + scroller.setContent(canvas); + scroller.setMinSize(canvas.getSize()); + + Font systemFont = display.getSystemFont(); + Map fonts = new HashMap<>(); + for (TextRow row : rows) { + FontData fontData = systemFont.getFontData()[0]; + fontData.setStyle(row.fontStyle()); + if (row.underline()) fontData.data.lfUnderline = 1; + if (row.strikeout()) fontData.data.lfStrikeOut = 1; + fonts.put(row, new Font(display, fontData)); + } + + // Mutable holder so the checkbox listeners can swap in a freshly rendered + // set of samples (and dispose the previous ones) whenever a toggle changes. + // Starts out empty; the initial samples are rendered by the refresh below. + AtomicReference> samplesHolder = new AtomicReference<>(Map.of()); + + Runnable refresh = () -> { + boolean advanced = advancedCheckbox.getSelection(); + + Map old = samplesHolder.getAndSet( + renderSamples(display, rows, fonts, sampleWidth, sampleHeight, advanced)); + old.values().forEach(Image::dispose); + + shell.setText("Text rendering comparison (advanced = " + advanced + ")"); + info.setText("GC.setAdvanced(" + advanced + ")" + + "\nToggle the checkbox below to compare plain GDI vs. GDI+ text rendering." + + " See the source comment for what to expect per row."); + shell.layout(true, true); + canvas.redraw(); + }; + + advancedCheckbox.addListener(SWT.Selection, e -> refresh.run()); + refresh.run(); + + canvas.addPaintListener(e -> { + GC gc = e.gc; + int y = 5; + for (TextRow row : rows) { + gc.drawText(row.label(), 5, y + (rowHeight - 4) / 4, SWT.DRAW_TRANSPARENT); + gc.drawImage(samplesHolder.get().get(row), labelWidth, y); + y += rowHeight; + } + }); + + canvas.addDisposeListener(e -> { + samplesHolder.get().values().forEach(Image::dispose); + fonts.values().forEach(Font::dispose); + }); + + shell.setSize(700, 500); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } +}