Skip to content

[Win32] Remove obsolete fallback for drawing text without GDI+ - #3100

Open
HeikoKlare wants to merge 2 commits into
eclipse-platform:masterfrom
vi-eclipse:issue-3091
Open

[Win32] Remove obsolete fallback for drawing text without GDI+#3100
HeikoKlare wants to merge 2 commits into
eclipse-platform:masterfrom
vi-eclipse:issue-3091

Conversation

@HeikoKlare

@HeikoKlare HeikoKlare commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Important

This PR is based on and should be merged after:

I propose to merge this PR early for 2026-12 M1

On Windows, GC.drawText() behaves counter-intuitively once GC.setAdvanced(true) is active: instead of letting GDI+ lay out the text, it has GDI compute the glyphs and their positions and uses GDI+ only to draw them. GDI+ performs the layout itself only for strings containing characters GDI has no glyph for.

That fallback has visible consequences. Most severely, text drawn with a font carrying an underline or strikeout decoration is not rendered at all, not merely undecorated, but entirely absent, which is the defect reported in #3091.

The fallback was introduced more than a decade ago because specific Chinese characters were not rendered correctly by GDI+ (bug 305815, bug 289244). Running the snippets from those bugs against current Windows shows none of those problems anymore.

How this change fixes it

GC.drawText() now lets GDI+ lay out the text whenever advanced mode is enabled, instead of only for strings GDI cannot provide glyphs for. useGDIP() is renamed to useGdipTextLayout(), since both of its outcomes draw with GDI+ and it only decides which engine performs the layout.

The previous behavior is preserved behind the org.eclipse.swt.internal.win32.useGDITextRenderingWithGDIP system property. It is purely a safety net: should anyone hit an unexpected text rendering regression, it lets them fall back instead of downgrading SWT. It is not API, may be removed at any point, and is intended to be removed once the new behavior has proven itself, together with the fallback code it guards. The property is evaluated once per GC rather than per
drawing operation, so it costs nothing on the text drawing path while a newly created GC still picks up a value changed at runtime.

GCWin32Tests gains six tests (14 executions) that pin down the resulting rendering: the underline/strikeout case from #3091, decoration across weight and slant combinations, mnemonic underlining, mirrored/RTL text, complex scripts and charsets (Arabic, Hebrew, CJK, Cyrillic, Greek, combining diacritics), and kerning-sensitive text width. Where a reference value is needed, plain non-advanced GDI rendering of the same text serves as the baseline, so the tests stay meaningful once the property and the fallback are gone. The fallback itself is deliberately left untested, as it is meant to be removed.

Consumers drawing text through an advanced GC will see rendering differences beyond the fixed defect, since layout is now performed by a different engine: kerning, tab stop width, mnemonic underlining and bidi/mirroring may all come out slightly differently.

Related

This PR is based on #3518, which fixes a pre-existing tab stop defect in the GDI+ text rendering path. That defect is independent of this change but would become visible to everyone once this path is used by default, so it was extracted for separate review. Until it is merged, its commit is part of this PR's diff and should be reviewed there.

How to test

Snippet395, added by #3518 and extended here, renders a set of text properties one row per property and switches between plain GDI, GDI+ and the legacy path at runtime, so the engines can be compared visually.

It also makes sense to test with GEF diagram with SWTGraphics, as those use GC's advanced mode when zooming.

Fixes #3091

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Test Results (win32)

   33 files  ±0     33 suites  ±0   4m 10s ⏱️ -59s
4 717 tests +1  4 641 ✅ +1  76 💤 ±0  0 ❌ ±0 
1 246 runs  +1  1 222 ✅ +1  24 💤 ±0  0 ❌ ±0 

Results for commit 7269ed0. ± Comparison against base commit 450ece0.

♻️ This comment has been updated with latest results.

@HeikoKlare

Copy link
Copy Markdown
Contributor Author

Interestingly, this change would also have an impact on some issues reported to GEF (@ptziegler fyi).

It seems like the following fix would be come obsolete as I see the behavior also fixed with this PR:

Without this change on 150% monitor:
image

With this change on 150% monitor:
image

For the underlying issue and the snippet posted there, I rather see a change that I would consider a regression caused by this PR:

This is how it looks without this change on 100%:
image

And this is how it looks with this change on 100%:
image

But comparing the position of the text on 100% monitor with its position on 150% monitor, the behavior after this change seems to be even more "correct".

Anyway, it seems like the Win32 GC.drawText() implementation has some severe flaws when using advanced mode / GDI+. I am just a bit afraid that simply removing all the code for falling back to GDI and completely relying on GDI+ for all text rendering when advanced mode is enabled may have unintended side effects. But checking some of the bugs for which this implementation has been made (such as https://bugs.eclipse.org/bugs/show_bug.cgi?id=289244 and https://bugs.eclipse.org/bugs/show_bug.cgi?id=305815) and running the snippets used back then without any issues after this change, hopefully GDI+ was improved so much throughout the years that all these workarounds are not necessary anymore.

@HeikoKlare

HeikoKlare commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Testing approach and additional findings

To validate this change more thoroughly and guard against regressions, I (with assistance from GitHub Copilot) added:

  • 19 additional automated regression tests in GCWin32Tests, comparing rendered output between the default (fixed) GDI+ text rendering path and the legacy fallback restored via the org.eclipse.swt.internal.win32.useGDITextRenderingWithGDIP system property. Beyond the underline/strikeout scenario from the reported issue, coverage now includes:

    • Style-decoration combinations (bold/italic + underline/strikeout)
    • Mnemonic/accelerator underlining
    • Mirrored/RTL text
    • Complex scripts and charsets (Arabic, Hebrew, CJK, Cyrillic, Greek, combining diacritics)
    • Kerning-sensitive text width
    • Tab stop expansion
  • Snippet395, a manual/visual companion snippet that renders the same set of text properties side-by-side, with two checkboxes to toggle at runtime (no restart needed) between plain GDI and GDI+ (advanced) rendering, and - while GDI+ is active - between the default and legacy text rendering path, to visually compare the output.

Finding: tab stop size regression

While building out this coverage, we found that the tab stop width computed by the GDI+ drawText path (drawTextGDIP) differs from both plain GDI and the legacy GDI+ fallback path: it used 8x the width of a single space glyph as measured by GDI+, whereas the other two paths use 8x GDI's average character width (TEXTMETRIC.tmAveCharWidth), matching the long-standing Win32 DrawText()/TabbedTextOut() convention. Since a space glyph is typically narrower than the average character width, this made tab stops shrink noticeably (roughly by half) whenever the default GDI+ path was used, compared to plain GDI and the legacy fallback.

We fixed this by having drawTextGDIP reuse the same TEXTMETRIC its caller already computes, so tab stops are now sized consistently across all three rendering paths.

Note: This tab-stop discrepancy appears to be a pre-existing bug in the GDI+ text rendering path (unrelated to the underline/strikeout issue this PR addresses) that this PR's change to always use that path simply made visible/impactful by default. It's probably worth splitting the tab-stop fix out into its own separate PR, since it fixes an independent, pre-existing defect rather than something introduced by this change - happy to do that if preferred.

snippet395

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Test Results

  212 files  ± 0    212 suites  ±0   27m 25s ⏱️ +31s
4 931 tests +34  4 907 ✅ +34   24 💤 ±0  0 ❌ ±0 
7 102 runs  +34  6 934 ✅ +34  168 💤 ±0  0 ❌ ±0 

Results for commit 08936c8. ± Comparison against base commit 83e249e.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates SWT’s Win32 GC.drawText() behavior in advanced mode to rely on GDI+ for text layout by default (fixing cases where decorated fonts could render no text at all), while keeping a temporary internal system-property escape hatch for reverting to the legacy “GDI computes glyph positions, GDI+ draws” path. It also adds a Windows-focused snippet and expands Win32 GC tests to pin down the intended rendering behavior, including tab-stop consistency and underline/strikeout regression coverage.

Changes:

  • Win32 GC.drawText() now prefers GDI+ text layout in advanced mode, with a per-GC cached internal property to opt back into legacy layout.
  • GDI+ tab-stop computation is aligned with Win32/GDI behavior by using TEXTMETRIC.tmAveCharWidth * 8 (removing the previous “space glyph width * 8” metric).
  • Adds/updates tooling: a Win32 comparison snippet (Snippet395), documentation entry, platform classpath exclusions, and additional GCWin32Tests.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet395.java Adds a Win32-focused UI snippet to compare GDI vs GDI+ text rendering paths and the legacy fallback.
examples/org.eclipse.swt.snippets/Snippets.md Registers Snippet395 in the snippet index with a preview link.
examples/org.eclipse.swt.snippets/.classpath_gtk Excludes the Win32-specific snippet from GTK snippet builds.
examples/org.eclipse.swt.snippets/.classpath_cocoa Excludes the Win32-specific snippet from Cocoa snippet builds.
bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java Switches advanced text layout to GDI+ by default, adds internal legacy fallback property, and fixes GDI+ tab-stop width to match GDI.
bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java Adds regression and behavior tests for advanced text rendering (underline/strikeout, mnemonic, mirroring, scripts, kerning, and tab stops).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

The GDI+ based text rendering path in GC.drawText() derived the DRAW_TAB
tab stop width from the width of a single space glyph, while the GDI
based path and plain (non-advanced) GDI both use eight times the font's
average character width, which is the convention Win32's own DrawText()
and TabbedTextOut() follow.

Those are two different metrics, not two ways of computing the same one:
in proportional fonts a space is roughly half the average character
width, so tab stops came out about half as wide whenever the GDI+ path
was taken. In monospace fonts the two nearly coincide, which is why the
discrepancy was easy to miss.

The GDI+ path is changed to reuse the GDI text metric already computed
by its caller, so that tab stops are sized consistently no matter which
path renders the text. This restores the behaviour established for the
GDI path in bug 289244 ("GDI+ drawText is not consistent with GDI"),
which the GDI+ fallback path reintroduced in bug 305815 had missed.

Add tests that pin down the tab stop width itself (eight average
character widths), its consistency across both rendering paths, and two
properties that must hold for either engine: consecutive tabs advance by
whole tab stops, and text following a tab starts at the same tab stop
regardless of what precedes the tab. They are measured from a leading
tab, which expands to exactly one tab stop and so is not diluted by the
slightly different glyph advances of the two engines, and cover both
proportional and monospace fonts.

Add Snippet395 as a manual/visual companion to the automated tests: it
renders a series of text properties, one row per property, and offers a
checkbox to switch between plain GDI and GDI+ (advanced) rendering at
runtime, without restarting, to visually compare the resulting output.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
A check in GC.drawText() makes the implementation fall back to the text
rendering based on glyphs and their positions calculated by GDI even
when GDI+ is used in all cases except when it contains specific
characters that GDI cannot handle. This was necessary more than a decade
ago because specific Chinese characters were not properly rendered by
GDI+. But this is quite non-intuitive as in case GDI+/advance mode is
enabled the consumer will usually expected GDI+ to be used for
rendering. In addition, the problematic scenario from back then works
fine now.

This change thus adapts the GC.drawText() implementation to use plain
GDI+ whenever it is enabled. The fallback code is preserved but only
executed if a newly introduced system property is enabled. This allows
to switch back to previous behavior in case unexpected regressions are
found. The property is to be removed in a future release in case no
regressions are found. It is evaluated once per GC rather than per
drawing operation, so that reading it does not add cost to text drawing,
while a newly created GC still picks up a value changed at runtime.

Rename useGDIP() to useGdipTextLayout(), since both of its outcomes draw
with GDI+ and it only decides which engine performs the layout.

Extend GCWin32Tests with additional regression tests covering not only
the previously fixed underline/strikeout invisibility issue, but also
style-decoration combinations, mnemonic underlining, mirrored/RTL text,
complex scripts and charsets (Arabic, Hebrew, CJK, Cyrillic, Greek,
combining diacritics) and kerning-sensitive text width. The tests assert
the correctness of the GDI+ rendering itself and, where a reference
value is required, use plain (non-advanced) GDI rendering of the same
text as baseline, so that they remain meaningful once the system
property is removed again. The fallback itself is deliberately left
untested, as it is meant to be removed once the new behavior has proven
itself.

Extend Snippet395 with a second checkbox toggling that system property
at runtime, so that the default and the legacy text rendering path can
be visually compared while GDI+ is active.

Fixes eclipse-platform#3091

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
@HeikoKlare
HeikoKlare marked this pull request as ready for review August 15, 2026 12:39
@HeikoKlare

Copy link
Copy Markdown
Contributor Author

This PR started as an experiment to find out whether the GDI fallback in GC.drawText() is still needed. Based on the validation done since, I consider it ready to be processed as a real change, and I have rewritten the PR description accordingly.

What changed since the experimental state:

  • The fallback is no longer deleted outright but preserved behind the org.eclipse.swt.internal.win32.useGDITextRenderingWithGDIP system property, so anyone hitting an unexpected regression can fall back without downgrading SWT. The property is a temporary escape hatch, not API, and is meant to be removed together with the fallback code once the new behavior has proven itself. It is evaluated once
    per GC, so it does not add cost to text drawing.
  • Automated coverage for the resulting rendering: decoration across weight/slant combinations, mnemonic underlining, mirrored/RTL text, complex scripts and charsets, and kerning-sensitive text width, using plain GDI rendering as the baseline where a reference is needed.
  • Snippet395 for comparing the rendering paths visually at runtime.
  • useGDIP() renamed to useGdipTextLayout(), since both of its outcomes draw with GDI+ and it only selects which engine performs the layout.

As suggested in my earlier comment, the tab stop fix is now extracted into its own PR, since it fixes a pre-existing defect in the GDI+ path rather than anything introduced here: #3518. This PR is based on it, so until #3518 is merged its commit is part of the diff here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Windows] Text is not displayed if the font has Underline or Strikeout and GC.setAdvanced(true)

2 participants