Skip to content

[Win32] Let GDI lay out text again unless GDI+ layout is required - #3579

Merged
HeikoKlare merged 1 commit into
eclipse-platform:masterfrom
HeikoKlare:gdi-text-layout-for-undecorated-fonts
Sep 19, 2026
Merged

HeikoKlare merged 1 commit into
eclipse-platform:masterfrom
HeikoKlare:gdi-text-layout-for-undecorated-fonts

Conversation

@HeikoKlare

@HeikoKlare HeikoKlare commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #3577

What is wrong

Since #3100 made GDI+ lay out text for advanced GCs, glyph advances come from the font's unhinted design metrics instead of the hinted, grid-fitted advances the platform uses everywhere else, that is in native controls, in TextLayout and in a non-advanced GC. Per glyph the two differ by less than a pixel, and across proportional text the deviations carry mixed signs and largely cancel, which is why it went unnoticed.

For digits they do not cancel. A font's figures are typically tabular, so all ten share one advance and hence one error that repeats with the same sign for every digit of a group. For Segoe UI at 9pt the digit advance is 1104/2048 em = 6.469px unhinted against 6px hinted, so every digit is 7.8% too wide and a group of 20 digits lands 10px, almost two digit widths, off from where the platform places it. Sub-pixel per glyph is substantial per group, which answers the obvious objection that a fraction of a pixel cannot be visible.

Width is only half of the symptom. Because the glyphs are still rasterized grid-fitted while the pen advances fractionally, the surplus is paid out in whole pixels at irregular intervals: for Segoe UI at 9pt consecutive gaps alternate between 7 and 6 pixels where GDI spaces every digit 6 pixels apart. A run of identical figures makes that rhythm obvious, so it reads as broken tracking rather than as slightly wider text.

This is documented GDI+ behavior rather than a defect. Microsoft KB 307208, "Why text appears different when drawn with GDIPlus versus GDI", states that "text layout with GDIPlus is resolution-independent": a string occupies the same fraction of an em at every resolution, at the price of the layout no longer matching the grid-fitted glyphs that are actually rasterized. Where those fall short of the designed width, the article's reconciliation ends in inserting blank pixels between glyphs, which is precisely the alternating gaps above.

Measured as the width of a 20 digit string, an advanced GC deviates from a non-advanced one as follows:

Font before after
Segoe UI -7 to +10 px 0 to 1 px
Arial -6 to -2 px 0 px
Times New Roman -10 to 0 px 0 px
Courier New -7 to +5 px 0 to 1 px
Consolas -9 to -4 px 0 px

How it is fixed

GDI becomes the text layout engine again, and GDI+ lays out text itself only where the glyph run cannot be drawn at all: for text containing characters GDI has no glyph for, where GDI would draw missing-glyph boxes, and for fonts with an underline or strikeout style. Both cases still draw with GDI+, so this only selects the layout engine, and the reason for using GDI+ in the first place, namely alpha blending, transforms and antialiasing over transparent backgrounds, is unaffected.

Letting GDI compute the advances and GDI+ draw the resulting glyph run is what DrawDriverString exists for, and it is the conclusion Microsoft drew for their own stack: Windows Forms moved off GDI+ text in .NET 2.0, where Application.SetCompatibleTextRenderingDefault defaults to the GDI based TextRenderer because "GDI calculates character spacing and word wrapping differently from GDI+". The alternative remedy KB 307208 suggests, the typographic StringFormat that SWT already passes plus TextRenderingHintAntiAlias, does even out the gaps by drawing unhinted glyphs at fractional positions, but ten Segoe UI digits still measure 64.7px against GDI's 60: it removes the artifact without restoring the platform's metrics, and gives up ClearType sharpness.

The decorated-font case is what actually caused #3091 in the first place: Graphics::DrawDriverString does not support font decoration and renders blank space instead of the glyphs, a known GDI+ limitation whose recommended remedy is to use DrawString for such fonts. Routing only those fonts to GDI+'s own layout keeps that fix intact and needs no decoration drawing of SWT's own; bold and italic are unaffected and keep using the glyph run.

The escape hatch introduced along with the GDI+ layout default is narrowed to that remaining purpose and renamed to org.eclipse.swt.internal.win32.useGDITextRenderingForDecoratedFonts. It now only reverts the decorated-font case to the GDI glyph run, at the price of that text rendering blank again, and remains an internal safety net that may be removed at any point in time.

A new test in GCWin32Tests pins the digit advances of an advanced GC to those of a non-advanced one across the existing proportional and monospace test fonts and several sizes, within the same one pixel rounding tolerance the tab stop tests use. The manual test SWTIssue3091_GDIPlusTextRendering gains digits in its sample strings so this class of regression is visible there, and follows the narrowed system property. Users should see no change other than text laid out as it was before #3100, since decorated fonts keep rendering exactly as they do today.

Verification against the reported symptom

IDE tab headers containing numbers, before and after:

image image

Rendering "Hello World 12345" through an advanced GC, as in the screenshots of #3577:

Configuration Plain Bold Italic
current master, the reported bug 99 106 98
master plus the workaround flag from the issue, the "old behavior" screenshot 97 104 95
this branch, no flag 97 104 95

This branch is identical to the workaround configuration, not only in total width but pixel by pixel, in all three rows.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Test Results (win32)

   35 files  ±0     35 suites  ±0   5m 3s ⏱️ -15s
4 939 tests +5  4 859 ✅ +5  80 💤 ±0  0 ❌ ±0 
1 463 runs  +5  1 437 ✅ +5  26 💤 ±0  0 ❌ ±0 

Results for commit 1bd0b16. ± Comparison against base commit 14da522.

♻️ 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.

🔵 Needs a closer look

The Win32 text-rendering changes warrant final human review.

Pull request overview

Restores GDI-based text layout for Win32 advanced GCs where possible, while retaining GDI+ handling for unsupported glyphs and decorated fonts.

Changes:

  • Updates GDI/GDI+ layout selection and narrows the fallback property.
  • Fixes tab-expanded text bounds handling.
  • Adds digit-spacing and tab-rendering regression coverage.
File summaries
File Summary
tests/org.eclipse.swt.tests.win32/ManualTests/org/eclipse/swt/tests/win32/snippets/SWTIssue3091_GDIPlusTextRendering.java Updates manual rendering comparisons, sample strings, and fallback property usage.
bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java Adjusts layout selection and tab bounds handling.
bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java Adds digit-spacing and tab-rendering regression tests.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

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

@HeikoKlare
HeikoKlare force-pushed the gdi-text-layout-for-undecorated-fonts branch 4 times, most recently from 536d142 to 4951edd Compare September 18, 2026 07:09
Since GDI+ text layout became the default for advanced GCs, glyph
advances are derived from the font's unhinted design metrics instead of
the hinted, grid-fitted advances the platform uses everywhere else, i.e.
in native controls, in TextLayout and in a non-advanced GC. Per glyph the
two differ by less than a pixel, and across proportional text the
deviations carry mixed signs and largely cancel, which is why this went
unnoticed.

For digits they do not cancel. A font's figures are typically tabular, so
all ten share one advance and hence one error that repeats with the same
sign for every digit of a group. What is a sub-pixel quantity per glyph
therefore becomes a substantial one per group: for Segoe UI at 9pt the
digit advance is 6.469px unhinted against 6px hinted, so a group of 20
digits comes out 10px, almost two digit widths, wider than the platform
places it. Across the test fonts and sizes the deviation reaches about 8%
of the group's width in either direction; it is now within the rounding
of the two extents.

The accumulated width is only half of the symptom. Since the glyphs are
still rasterized grid-fitted while the pen advances fractionally, the
surplus is paid out in whole pixels at irregular intervals, so that the
gaps within a group alternate, e.g. 7 6 7 6 for Segoe UI at 9pt where GDI
spaces every digit 6 pixels apart. A run of identical figures makes that
rhythm obvious, which is why this reads as broken tracking rather than as
slightly wider text.

This change therefore restores GDI as the text layout engine and limits
GDI+'s own layout to the cases where the glyph run cannot be drawn at
all: text containing characters GDI has no glyph for, and fonts with an
underline or strikeout style. The latter is the actual cause behind
eclipse-platform#3091 :
Graphics_DrawDriverString does not support font decoration and draws
blank space instead of the glyphs, which is a documented GDI+ limitation
whose recommended remedy is to use Graphics_DrawString for those fonts.
Both cases still draw with GDI+, so this only selects which engine
performs the layout, and decorated text keeps rendering exactly as it
does today.

Narrow the escape hatch introduced along with the GDI+ layout default
accordingly, and rename it to reflect its remaining scope: it now only
reverts the decorated-font case to the GDI glyph run, at the price of
that text rendering blank again. It remains an internal safety net that
may be removed at any point in time.

Fixes eclipse-platform#3577

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
@HeikoKlare
HeikoKlare force-pushed the gdi-text-layout-for-undecorated-fonts branch from 4951edd to 1bd0b16 Compare September 19, 2026 11:14
@HeikoKlare

Copy link
Copy Markdown
Contributor Author

This is only a partial revert #3100, so we simply restore previous behavior, which makes the change safe.

The bug is also visible in the Git history view when having commit messages or tags with sequences of multiple digits, where you then see the bad kerning (e.g., in the binaries version number and the I-Build versions):
image

@HeikoKlare
HeikoKlare merged commit 2fb03ab into eclipse-platform:master Sep 19, 2026
17 checks passed
@HeikoKlare
HeikoKlare deleted the gdi-text-layout-for-undecorated-fonts branch September 19, 2026 11:33
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.

[Win32] Broken text kerning with GDI+

2 participants