SymbolIcon renders the wrong glyph for all SymbolRegular/SymbolFilled members above U+FFFF (supplementary plane)
Describe the bug
SymbolIcon (and any consumer of SymbolExtensions.GetString) renders the wrong glyph for every SymbolRegular/SymbolFilled member whose codepoint is in the Unicode supplementary plane (> U+FFFF), e.g. SymbolRegular.CoinStack24, the whole CoinMultiple* family, and many newer Fluent icons.
The conversion from the enum value to the glyph string does not compose a UTF-16 surrogate pair for scalar values above U+FFFF, so the font's cmap can never select the intended glyph. The glyph does exist in the bundled font — it simply can't be addressed.
This affects 2863 of 9235 SymbolRegular members (~31%), since Fluent System Icons assigns its newer icons to the Supplementary Private Use Area-A (U+F0000–U+FFFFD).
To Reproduce
Place any supplementary-plane symbol in XAML:
<ui:SymbolIcon Symbol="CoinStack24" FontSize="28" />Run the app and look at the rendered icon.
Expected behavior
The "coin stack" glyph is displayed (it is present in the bundled
FluentSystemIcons-Regular.ttf, glyphIndex 2368).
Screenshots
CoinStack24 (= U+F0665) renders as the Arabic-Indic digit five ٥ plus a control
character, instead of the coin-stack glyph. Any BMP symbol such as Savings24
(= U+F686) renders correctly in the same place.
OS version
Windows 11 Pro 10.0.26200
.NET version
.NET 10 (net10.0-windows) — also reproduces on net8.0-windows
WPF-UI NuGet version
WPF-UI 4.3.0
Additional context
Root cause
src/Wpf.Ui/Extensions/SymbolExtensions.cs (both the SymbolRegular and SymbolFilled
overloads); used by SymbolIcon via SetCurrentValue(GlyphProperty, Symbol.GetString()):
public static string GetString(this SymbolRegular icon)
{
return Encoding.Unicode.GetString(BitConverter.GetBytes((int)icon)).TrimEnd('\0');
}This reinterprets the 4 raw bytes of the 32-bit enum value as two independent UTF-16LE code units instead of encoding a UTF-32 scalar as a surrogate pair:
- BMP (≤ U+FFFF),
Money24=0xF550: bytes50 F5 00 00→[U+F550, U+0000]→TrimEnd('\0')→[U+F550]. ✅ Works (by accident). - Supplementary,
CoinStack24=0xF0665: bytes65 06 0F 00→[U+0665, U+000F]. The correct encoding ofU+F0665is the surrogate pair[U+DB81, U+DE65].TrimEnd('\0')does not stripU+000F. ❌ The format-12cmapentry is never reached.
Verification (GetString invoked via reflection on Wpf.Ui.dll 4.3.0)
| Symbol | enum value | GetString result | Correct value |
|---|---|---|---|
Money24 |
0xF550 (BMP) |
[U+F550] (len 1) ✅ |
[U+F550] |
CoinStack24 |
0xF0665 (supp.) |
[U+0665, U+000F] (len 2) ❌ |
[U+DB81, U+DE65] |
The bundled font maps 2869 codepoints above U+FFFF via a cmap format-12 subtable, so the
font is correct; the defect is purely in GetString.
Proposed fix
char.ConvertFromUtf32 handles both BMP and supplementary scalars correctly (and makes
TrimEnd unnecessary):
- return Encoding.Unicode.GetString(BitConverter.GetBytes((int)icon)).TrimEnd('\0');
+ return char.ConvertFromUtf32((int)icon);(apply to the SymbolFilled overload too)
Workaround
Use a BMP symbol, or render the glyph explicitly via FontIcon + an XML character
reference (parsed into a correct surrogate pair):
<ui:FontIcon FontFamily="pack://application:,,,/Wpf.Ui;component/Resources/Fonts/#FluentSystemIcons-Regular"
Glyph="󰙥" FontSize="28" />Source: lepoco/wpfui