mirror of https://github.com/icsharpcode/ILSpy.git
4 changed files with 293 additions and 0 deletions
@ -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)); |
||||
} |
||||
} |
||||
@ -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"); |
||||
} |
||||
} |
||||
} |
||||
@ -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 > Accessibility > 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…
Reference in new issue