diff --git a/ILSpy.Tests.Windows/WindowsSystemFontTests.cs b/ILSpy.Tests.Windows/WindowsSystemFontTests.cs
new file mode 100644
index 0000000000..39641a4322
--- /dev/null
+++ b/ILSpy.Tests.Windows/WindowsSystemFontTests.cs
@@ -0,0 +1,44 @@
+// Copyright (c) 2026 Christoph Wille
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using ICSharpCode.ILSpy.Util;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Windows;
+
+///
+/// Verifies the NONCLIENTMETRICS message-font reader used to follow the Windows system UI font.
+/// The exact values depend on the machine (the accessibility "Text size" setting scales them),
+/// so the assertions only pin down what must hold everywhere: a face name is present and the
+/// size is a sane DIP value (metrics are requested at 96 DPI, so 12 on a default install,
+/// larger when text scaling is active).
+///
+[TestFixture]
+public class WindowsSystemFontTests
+{
+ [Test]
+ public void MessageFontIsReadable()
+ {
+ bool available = WindowsSystemFont.TryGetMessageFont(out var faceName, out var fontSize);
+
+ Assert.That(available, Is.True);
+ Assert.That(faceName, Is.Not.Null.And.Not.Empty);
+ Assert.That(fontSize, Is.InRange(9.0, 48.0));
+ }
+}
diff --git a/ILSpy.Tests/UiFontTests.cs b/ILSpy.Tests/UiFontTests.cs
new file mode 100644
index 0000000000..a8a6b6768d
--- /dev/null
+++ b/ILSpy.Tests/UiFontTests.cs
@@ -0,0 +1,83 @@
+// Copyright (c) 2026 Christoph Wille
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Headless.NUnit;
+using Avalonia.Styling;
+
+using AwesomeAssertions;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests;
+
+///
+/// Exercises App.ApplyUiFont (the system-UI-font override) with a synthetic size, since the
+/// production path reads machine-dependent NONCLIENTMETRICS. The interesting case is
+/// ContextMenu: the Simple theme pins its FontSize to the theme's FontSizeNormal resource,
+/// which outranks the inherited value that covers windows and menu-bar dropdowns, so a
+/// context menu silently stays at 12 unless the override also reaches that resource.
+///
+[TestFixture]
+public class UiFontTests
+{
+ // Distinct from every size the Simple theme uses (10/12/16) so a match proves the override.
+ const double TestFontSize = 20;
+
+ [AvaloniaTest]
+ public void UiFontReachesWindowAndContextMenu()
+ {
+ var app = Application.Current!;
+ int styleCount = app.Styles.Count;
+ bool hadFontSizeNormal = app.Resources.TryGetValue("FontSizeNormal", out var previousFontSizeNormal);
+
+ try
+ {
+ App.ApplyUiFont(app, "Segoe UI", TestFontSize);
+
+ var contextMenu = new ContextMenu {
+ Items = { new MenuItem { Header = "Item" } },
+ };
+ var target = new Button { ContextMenu = contextMenu };
+ var window = new Window { Content = target };
+ window.Show();
+
+ window.FontSize.Should().Be(TestFontSize, "the TopLevel style must reach windows");
+
+ contextMenu.Open(target);
+ contextMenu.FontSize.Should().Be(TestFontSize,
+ "the override must beat the Simple theme's FontSizeNormal pin on ContextMenu");
+
+ window.Close();
+ }
+ finally
+ {
+ // App styles/resources are per-assembly shared state (ResetAppState rebuilds the MEF
+ // container, not the Application); undo so later tests keep the default 12.
+ while (app.Styles.Count > styleCount)
+ app.Styles.RemoveAt(app.Styles.Count - 1);
+ if (hadFontSizeNormal)
+ app.Resources["FontSizeNormal"] = previousFontSizeNormal;
+ else
+ app.Resources.Remove("FontSizeNormal");
+ }
+ }
+}
diff --git a/ILSpy/App.axaml.cs b/ILSpy/App.axaml.cs
index ba66e8aacd..57dcdac1c4 100644
--- a/ILSpy/App.axaml.cs
+++ b/ILSpy/App.axaml.cs
@@ -23,8 +23,12 @@
using System.Threading.Tasks;
using Avalonia;
+using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Controls.Primitives;
using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.Styling;
using Avalonia.Threading;
using ICSharpCode.ILSpyX.Settings;
@@ -32,6 +36,7 @@
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.AssemblyTree;
using ICSharpCode.ILSpy.Themes;
+using ICSharpCode.ILSpy.Util;
using ICSharpCode.ILSpy.Views;
namespace ICSharpCode.ILSpy
@@ -85,6 +90,8 @@ public override void OnFrameworkInitializationCompleted()
StartupExceptions.Items.Add(new ExceptionData(ex));
}
+ ApplySystemFont();
+
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
MainWindow? mainWindow = null;
@@ -123,6 +130,49 @@ public override void OnFrameworkInitializationCompleted()
base.OnFrameworkInitializationCompleted();
}
+ // Follows the Windows system UI font instead of Avalonia's built-in Segoe UI 12 default.
+ // The two coincide on a stock install, but the accessibility "Text size" setting scales
+ // the system metrics without changing DPI, and only apps that read them honor it.
+ // No-op on non-Windows platforms.
+ //
+ // The following deliberately do NOT follow the system font:
+ // - the decompiled-code editor: user-configurable via DisplaySettings.SelectedFont/Size;
+ // - controls with an explicit FontSize in their XAML: search-pane results, the resource
+ // string/object tables (whose fixed RowHeight is tuned to that size), the zoom-buttons
+ // overlay, the NuGet feed dialog, the XML-doc renderer, and the startup-error /
+ // assertion dialogs;
+ // - Simple-theme controls sized via FontSizeSmall/FontSizeLarge rather than
+ // FontSizeNormal: TabItem headers (Options dialog, already larger than body text)
+ // and Calendar buttons (unused in ILSpy).
+ void ApplySystemFont()
+ {
+ if (!WindowsSystemFont.TryGetMessageFont(out var faceName, out var fontSize))
+ return;
+
+ ApplyUiFont(this, faceName, fontSize);
+ AppLog.Mark($"System font applied: {faceName} @ {fontSize}");
+ }
+
+ // Styled on every TopLevel (not just Window) so popup surfaces -- menu dropdowns, context
+ // menus, tooltips -- get the font directly rather than relying on inheritance across the
+ // popup boundary. Internal so the headless test suite can exercise it with a synthetic
+ // size instead of the machine-dependent system metrics.
+ internal static void ApplyUiFont(Application app, string faceName, double fontSize)
+ {
+ var fontStyle = new Style(x => x.Is());
+ fontStyle.Setters.Add(new Setter(TemplatedControl.FontFamilyProperty, new FontFamily(faceName)));
+ fontStyle.Setters.Add(new Setter(TemplatedControl.FontSizeProperty, fontSize));
+ app.Styles.Add(fontStyle);
+
+ // The Simple theme pins FontSize on a few control themes via its FontSizeNormal
+ // resource (ContextMenu is the one ILSpy hits; also PopupRoot/Window, where the
+ // style above already outranks it). A control-theme setter beats the inherited
+ // value, so shadow the resource at app level -- application resources win the
+ // DynamicResource lookup over theme resources. FontSizeSmall/FontSizeLarge stay
+ // untouched; their consumers are listed on ApplySystemFont.
+ app.Resources["FontSizeNormal"] = fontSize;
+ }
+
// Resolves where ILSpy.xml is loaded from. Shared by normal startup and the single-instance
// gate in Program.Main, which reads the settings before Avalonia starts.
internal static void ConfigureSettingsFilePathProvider(CommandLineArguments commandLineArguments)
diff --git a/ILSpy/Util/WindowsSystemFont.cs b/ILSpy/Util/WindowsSystemFont.cs
new file mode 100644
index 0000000000..07524bae43
--- /dev/null
+++ b/ILSpy/Util/WindowsSystemFont.cs
@@ -0,0 +1,116 @@
+// Copyright (c) 2026 Christoph Wille
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Runtime.InteropServices;
+
+#pragma warning disable CA1060 // Move pinvokes to native methods class
+
+namespace ICSharpCode.ILSpy.Util
+{
+ ///
+ /// Reads the Windows system UI font (the NONCLIENTMETRICS message font). Avalonia's built-in
+ /// default (Segoe UI at 12 DIP) matches a stock Windows install, but the accessibility
+ /// "Text size" setting (Settings > Accessibility > Text size) scales these metrics
+ /// without changing DPI, and only apps that read them follow it.
+ ///
+ public static partial class WindowsSystemFont
+ {
+ const uint SPI_GETNONCLIENTMETRICS = 0x0029;
+
+ [StructLayout(LayoutKind.Sequential)]
+ unsafe struct LOGFONTW
+ {
+ public int lfHeight;
+ public int lfWidth;
+ public int lfEscapement;
+ public int lfOrientation;
+ public int lfWeight;
+ public byte lfItalic;
+ public byte lfUnderline;
+ public byte lfStrikeOut;
+ public byte lfCharSet;
+ public byte lfOutPrecision;
+ public byte lfClipPrecision;
+ public byte lfQuality;
+ public byte lfPitchAndFamily;
+ public fixed char lfFaceName[32];
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ struct NONCLIENTMETRICSW
+ {
+ public uint cbSize;
+ public int iBorderWidth;
+ public int iScrollWidth;
+ public int iScrollHeight;
+ public int iCaptionWidth;
+ public int iCaptionHeight;
+ public LOGFONTW lfCaptionFont;
+ public int iSmCaptionWidth;
+ public int iSmCaptionHeight;
+ public LOGFONTW lfSmCaptionFont;
+ public int iMenuWidth;
+ public int iMenuHeight;
+ public LOGFONTW lfMenuFont;
+ public LOGFONTW lfStatusFont;
+ public LOGFONTW lfMessageFont;
+ public int iPaddedBorderWidth;
+ }
+
+ [LibraryImport("user32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool SystemParametersInfoForDpi(uint uiAction, uint uiParam, ref NONCLIENTMETRICSW pvParam, uint fWinIni, uint dpi);
+
+ ///
+ /// Gets the system message font as an Avalonia-ready (family name, size in DIPs) pair.
+ /// Returns false on non-Windows platforms or if the metrics cannot be read. The metrics
+ /// are requested at 96 DPI, so the size is in device-independent pixels regardless of
+ /// display scaling (which Avalonia applies separately).
+ ///
+ public static unsafe bool TryGetMessageFont(out string faceName, out double fontSize)
+ {
+ faceName = string.Empty;
+ fontSize = 0;
+
+ // SystemParametersInfoForDpi requires Windows 10 1607.
+ if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 14393))
+ return false;
+
+ var metrics = new NONCLIENTMETRICSW { cbSize = (uint)sizeof(NONCLIENTMETRICSW) };
+ if (!SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, metrics.cbSize, ref metrics, 0, 96))
+ return false;
+
+ // A negative lfHeight is the character height (the usual case for the message font);
+ // a positive one is the cell height. Either way the magnitude is the pixel size at
+ // the requested DPI.
+ int height = Math.Abs(metrics.lfMessageFont.lfHeight);
+ if (height <= 0)
+ return false;
+
+ var nameBuffer = new ReadOnlySpan(metrics.lfMessageFont.lfFaceName, 32);
+ int terminator = nameBuffer.IndexOf('\0');
+ faceName = new string(terminator >= 0 ? nameBuffer[..terminator] : nameBuffer);
+ if (faceName.Length == 0)
+ return false;
+
+ fontSize = height;
+ return true;
+ }
+ }
+}