Browse Source

Merge c7273d528d into c1abc29765

pull/3987/merge
Christoph Wille 2 days ago committed by GitHub
parent
commit
2635a7573e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 44
      ILSpy.Tests.Windows/WindowsSystemFontTests.cs
  2. 83
      ILSpy.Tests/UiFontTests.cs
  3. 50
      ILSpy/App.axaml.cs
  4. 116
      ILSpy/Util/WindowsSystemFont.cs

44
ILSpy.Tests.Windows/WindowsSystemFontTests.cs

@ -0,0 +1,44 @@ @@ -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;
/// <summary>
/// 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).
/// </summary>
[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));
}
}

83
ILSpy.Tests/UiFontTests.cs

@ -0,0 +1,83 @@ @@ -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;
/// <summary>
/// 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.
/// </summary>
[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");
}
}
}

50
ILSpy/App.axaml.cs

@ -23,8 +23,12 @@ using System.IO; @@ -23,8 +23,12 @@ using System.IO;
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.ILSpyX.Settings; @@ -32,6 +36,7 @@ using ICSharpCode.ILSpyX.Settings;
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 @@ namespace ICSharpCode.ILSpy @@ -85,6 +90,8 @@ namespace ICSharpCode.ILSpy
StartupExceptions.Items.Add(new ExceptionData(ex));
}
ApplySystemFont();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
MainWindow? mainWindow = null;
@ -123,6 +130,49 @@ namespace ICSharpCode.ILSpy @@ -123,6 +130,49 @@ namespace ICSharpCode.ILSpy
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<TopLevel>());
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)

116
ILSpy/Util/WindowsSystemFont.cs

@ -0,0 +1,116 @@ @@ -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
{
/// <summary>
/// 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 &gt; Accessibility &gt; Text size) scales these metrics
/// without changing DPI, and only apps that read them follow it.
/// </summary>
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);
/// <summary>
/// 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).
/// </summary>
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<char>(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;
}
}
}
Loading…
Cancel
Save