From cb461ab04383e7421b9a9da95129ae73fe15d692 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 14 Sep 2023 11:58:55 +0200 Subject: [PATCH 01/18] Allow multiple font sources per FontFamily and make sure combinations of system and embedded fonts can be used (#12871) --- .../Media/CompositeFontFamilyKey.cs | 16 +++ src/Avalonia.Base/Media/FontFamily.cs | 104 ++++++++++----- src/Avalonia.Base/Media/FontManager.cs | 126 ++++++++++++------ .../Media/FontSourceIdentifier.cs | 17 +++ .../Media/Fonts/FamilyNameCollection.cs | 14 ++ .../Media/Fonts/FontCollectionBase.cs | 6 +- .../Media/FontFamilyTests.cs | 2 +- .../Media/FontManagerImplTests.cs | 12 +- .../Media/FontManagerTests.cs | 78 +++++++++++ 9 files changed, 286 insertions(+), 89 deletions(-) create mode 100644 src/Avalonia.Base/Media/CompositeFontFamilyKey.cs create mode 100644 src/Avalonia.Base/Media/FontSourceIdentifier.cs diff --git a/src/Avalonia.Base/Media/CompositeFontFamilyKey.cs b/src/Avalonia.Base/Media/CompositeFontFamilyKey.cs new file mode 100644 index 0000000000..eed71cd4e2 --- /dev/null +++ b/src/Avalonia.Base/Media/CompositeFontFamilyKey.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media.Fonts; + +namespace Avalonia.Media +{ + internal class CompositeFontFamilyKey : FontFamilyKey + { + public CompositeFontFamilyKey(Uri source, FontFamilyKey[] keys) : base(source, null) + { + Keys = keys; + } + + public IReadOnlyList Keys { get; } + } +} diff --git a/src/Avalonia.Base/Media/FontFamily.cs b/src/Avalonia.Base/Media/FontFamily.cs index 498bcd43a0..80365aaf4d 100644 --- a/src/Avalonia.Base/Media/FontFamily.cs +++ b/src/Avalonia.Base/Media/FontFamily.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using Avalonia.Media.Fonts; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -34,19 +36,42 @@ namespace Avalonia.Media throw new ArgumentNullException(nameof(name)); } - var fontFamilySegment = GetFontFamilyIdentifier(name); + var fontSources = GetFontSourceIdentifier(name); - if (fontFamilySegment.Source != null) + FamilyNames = new FamilyNameCollection(fontSources); + + if (fontSources.Count == 1) { - if (baseUri != null && !baseUri.IsAbsoluteUri) + if(fontSources[0].Source is Uri source) { - throw new ArgumentException("Base uri must be an absolute uri.", nameof(baseUri)); - } + if (baseUri != null && !baseUri.IsAbsoluteUri) + { + throw new ArgumentException("Base uri must be an absolute uri.", nameof(baseUri)); + } - Key = new FontFamilyKey(fontFamilySegment.Source, baseUri); + Key = new FontFamilyKey(source, baseUri); + } } + else + { + var keys = new FontFamilyKey[fontSources.Count]; + + for (int i = 0; i < fontSources.Count; i++) + { + var fontSource = fontSources[i]; - FamilyNames = new FamilyNameCollection(fontFamilySegment.Name); + if(fontSource.Source is not null) + { + keys[i] = new FontFamilyKey(fontSource.Source, baseUri); + } + else + { + keys[i] = new FontFamilyKey(new Uri(FontManager.SystemFontScheme + ":" + fontSource.Name, UriKind.Absolute)); + } + } + + Key = new CompositeFontFamilyKey(new Uri(FontManager.CompositeFontScheme + ":" + name, UriKind.Absolute), keys); + } } /// @@ -88,44 +113,49 @@ namespace Avalonia.Media return new FontFamily(s); } - private struct FontFamilyIdentifier + private static FrugalStructList GetFontSourceIdentifier(string name) { - public FontFamilyIdentifier(string name, Uri? source) - { - Name = name; - Source = source; - } - - public string Name { get; } - - public Uri? Source { get; } - } + var result = new FrugalStructList(1); - private static FontFamilyIdentifier GetFontFamilyIdentifier(string name) - { - var segments = name.Split('#'); + var segments = name.Split(','); - switch (segments.Length) + for (int i = 0; i < segments.Length; i++) { - case 1: - { - return new FontFamilyIdentifier(segments[0], null); - } + var segment = segments[i]; + var innerSegments = segment.Split('#'); - case 2: - { - var source = segments[0].StartsWith("/", StringComparison.Ordinal) - ? new Uri(segments[0], UriKind.Relative) - : new Uri(segments[0], UriKind.RelativeOrAbsolute); + FontSourceIdentifier identifier; - return new FontFamilyIdentifier(segments[1], source); - } + switch (innerSegments.Length) + { + case 1: + { + identifier = new FontSourceIdentifier(innerSegments[0].Trim(), null); + break; + } + + case 2: + { + var source = innerSegments[0].StartsWith("/", StringComparison.Ordinal) + ? new Uri(innerSegments[0], UriKind.Relative) + : new Uri(innerSegments[0], UriKind.RelativeOrAbsolute); + + identifier = new FontSourceIdentifier(innerSegments[1].Trim(), source); + + break; + } + + default: + { + identifier = new FontSourceIdentifier(name, null); + break; + } + } - default: - { - return new FontFamilyIdentifier(name, null); - } + result.Add(identifier); } + + return result; } /// diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 17d1984286..850d0011f5 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -15,9 +15,11 @@ namespace Avalonia.Media /// public sealed class FontManager { - internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts"); + internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts", UriKind.Absolute); public const string FontCollectionScheme = "fonts"; + public const string SystemFontScheme = "systemfont"; + public const string CompositeFontScheme = "compositefont"; private readonly ConcurrentDictionary _fontCollections = new ConcurrentDictionary(); private readonly IReadOnlyList? _fontFallbacks; @@ -95,69 +97,86 @@ namespace Avalonia.Media var fontFamily = typeface.FontFamily; - if(typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName) + if (typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName) { return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); } - if (fontFamily.Key is FontFamilyKey key) + if (fontFamily.Key is FontFamilyKey) { - var source = key.Source; - - if (!source.IsAbsoluteUri) + if (fontFamily.Key is CompositeFontFamilyKey compositeKey) { - if (key.BaseUri == null) + for (int i = 0; i < compositeKey.Keys.Count; i++) { - throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null."); - } + var key = compositeKey.Keys[i]; - source = new Uri(key.BaseUri, source); + var familyName = fontFamily.FamilyNames[i]; + + if (TryGetGlyphTypefaceByKeyAndName(typeface, key, familyName, out glyphTypeface) && + glyphTypeface.FamilyName.Contains(familyName)) + { + return true; + } + } } - - if (!_fontCollections.TryGetValue(source, out var fontCollection) && (source.IsAbsoluteResm() || source.IsAvares())) + else { - var embeddedFonts = new EmbeddedFontCollection(source, source); - - embeddedFonts.Initialize(PlatformImpl); - - if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts)) + if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, fontFamily.FamilyNames.PrimaryFamilyName, out glyphTypeface)) { - fontCollection = embeddedFonts; + return true; } - } - if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, - typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) + return false; + } + } + else + { + if (SystemFonts.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { return true; } + } - if (!fontFamily.FamilyNames.HasFallbacks) - { - return false; - } + if (typeface.FontFamily == DefaultFontFamily) + { + return false; } - for (var i = 0; i < fontFamily.FamilyNames.Count; i++) + //Nothing was found so use the default + return TryGetGlyphTypeface(new Typeface(FontFamily.DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); + } + + private bool TryGetGlyphTypefaceByKeyAndName(Typeface typeface, FontFamilyKey key, string familyName, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + var source = key.Source; + + if (source.Scheme == SystemFontScheme) { - var familyName = fontFamily.FamilyNames[i]; + return SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface); + } - if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) + if (!source.IsAbsoluteUri) + { + if (key.BaseUri == null) { - if (!fontFamily.FamilyNames.HasFallbacks || glyphTypeface.FamilyName != DefaultFontFamily.Name) - { - return true; - } + throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null."); } + + source = new Uri(key.BaseUri, source); } - if(typeface.FontFamily == DefaultFontFamily) + if (TryGetFontCollection(source, out var fontCollection) && + fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { - return false; + if (glyphTypeface.FamilyName.Contains(familyName)) + { + return true; + } } - //Nothing was found so use the default - return TryGetGlyphTypeface(new Typeface(FontFamily.DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); + glyphTypeface = null; + + return false; } /// @@ -230,18 +249,17 @@ namespace Avalonia.Media } //Try to match against fallbacks first - if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks) + if (fontFamily != null && fontFamily.Key is CompositeFontFamilyKey compositeKey) { - for (int i = 1; i < fontFamily.FamilyNames.Count; i++) + for (int i = 0; i < compositeKey.Keys.Count; i++) { + var key = compositeKey.Keys[i]; var familyName = fontFamily.FamilyNames[i]; - foreach (var fontCollection in _fontCollections.Values) + if (TryGetFontCollection(key.Source, out var fontCollection) && + fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) { - if (fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) - { - return true; - }; + return true; } } } @@ -249,5 +267,27 @@ namespace Avalonia.Media //Try to find a match with the system font manager return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface); } + + private bool TryGetFontCollection(Uri source, [NotNullWhen(true)] out IFontCollection? fontCollection) + { + if(source.Scheme == SystemFontScheme) + { + source = SystemFontsKey; + } + + if (!_fontCollections.TryGetValue(source, out fontCollection) && (source.IsAbsoluteResm() || source.IsAvares())) + { + var embeddedFonts = new EmbeddedFontCollection(source, source); + + embeddedFonts.Initialize(PlatformImpl); + + if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts)) + { + fontCollection = embeddedFonts; + } + } + + return fontCollection != null; + } } } diff --git a/src/Avalonia.Base/Media/FontSourceIdentifier.cs b/src/Avalonia.Base/Media/FontSourceIdentifier.cs new file mode 100644 index 0000000000..a4c89bbcb8 --- /dev/null +++ b/src/Avalonia.Base/Media/FontSourceIdentifier.cs @@ -0,0 +1,17 @@ +using System; + +namespace Avalonia.Media +{ + internal readonly record struct FontSourceIdentifier + { + public FontSourceIdentifier(string name, Uri? source) + { + Name = name; + Source = source; + } + + public string Name { get; init; } + + public Uri? Source { get; init; } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs index f2350f5aea..dabe935b76 100644 --- a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs @@ -28,6 +28,20 @@ namespace Avalonia.Media.Fonts HasFallbacks = _names.Length > 1; } + internal FamilyNameCollection(FrugalStructList fontSources) + { + _names = new string[fontSources.Count]; + + for (int i = 0; i < fontSources.Count; i++) + { + _names[i] = fontSources[i].Name; + } + + PrimaryFamilyName = _names[0]; + + HasFallbacks = _names.Length > 1; + } + private static string[] SplitNames(string names) #if NET6_0_OR_GREATER => names.Split(',', StringSplitOptions.TrimEntries); diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index 713b3dafcd..3daa19c788 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -34,7 +34,7 @@ namespace Avalonia.Media.Fonts { if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) { - match = new Typeface(glyphTypeface.FamilyName, style, weight, stretch); + match = new Typeface(Key.AbsoluteUri + "#" + glyphTypeface.FamilyName, style, weight, stretch); return true; } @@ -45,9 +45,9 @@ namespace Avalonia.Media.Fonts { if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface)) { - if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + if (glyphTypeface.FamilyName.Contains(familyName) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) { - match = new Typeface(familyName, style, weight, stretch); + match = new Typeface(Key.AbsoluteUri + "#" + familyName, style, weight, stretch); return true; } diff --git a/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs index 1f8ae9bd8b..73c46a9295 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs @@ -75,7 +75,7 @@ namespace Avalonia.Base.UnitTests.Media Assert.Equal("Courier New", fontFamily.Name); - Assert.Equal(2, fontFamily.FamilyNames.Count()); + Assert.Equal(2, fontFamily.FamilyNames.Count); Assert.Equal("Times New Roman", fontFamily.FamilyNames.Last()); } diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index 81ac9030bf..15e85750ea 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Direct2D1.Media; +using Avalonia.Direct2D1.Media; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -17,8 +16,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var glyphTypeface = - new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface; + var typeface = new Typeface(new FontFamily("A, B, Arial")); + + var glyphTypeface = typeface.GlyphTypeface; Assert.Equal("Arial", glyphTypeface.FamilyName); } @@ -31,7 +31,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface; + var typeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold); + + var glyphTypeface = typeface.GlyphTypeface; Assert.Equal("Arial", glyphTypeface.FamilyName); diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index 0818510bc2..98971426f1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -139,5 +139,83 @@ namespace Avalonia.Skia.UnitTests.Media } } } + + [Fact] + public void Should_Load_Embedded_Fallbacks() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + using (AvaloniaLocator.EnterScope()) + { + var fontFamily = FontFamily.Parse("NotFound, " + s_fontUri); + + var typeface = new Typeface(fontFamily); + + var glyphTypeface = typeface.GlyphTypeface; + + Assert.NotNull(glyphTypeface); + + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); + } + } + } + + [Fact] + public void Should_Match_Chararcter_Width_Embedded_Fallbacks() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + using (AvaloniaLocator.EnterScope()) + { + var fontFamily = FontFamily.Parse("NotFound, " + s_fontUri); + + Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface)); + + var glyphTypeface = typeface.GlyphTypeface; + + Assert.NotNull(glyphTypeface); + + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); + } + } + } + + [Fact] + public void Should_Match_Chararcter_From_SystemFonts() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + using (AvaloniaLocator.EnterScope()) + { + Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var typeface)); + + var glyphTypeface = typeface.GlyphTypeface; + + Assert.NotNull(glyphTypeface); + + Assert.Equal(FontManager.Current.DefaultFontFamily.Name, glyphTypeface.FamilyName); + } + } + } + + [Fact] + public void Should_Match_Chararcter_Width_Fallbacks() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + using (AvaloniaLocator.EnterScope()) + { + var fontFamily = FontFamily.Parse("NotFound, Unknown"); + + Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface)); + + var glyphTypeface = typeface.GlyphTypeface; + + Assert.NotNull(glyphTypeface); + + Assert.Equal(FontManager.Current.DefaultFontFamily.Name, glyphTypeface.FamilyName); + } + } + } } } From 09c5f20984a551fc51031544df576cc423eb27dc Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 14 Sep 2023 17:45:36 +0300 Subject: [PATCH 02/18] [X11] Use Xft.dpi and QT_*** environment variable to get screen scaling (#12880) --- .../Screens/X11Screen.Providers.cs | 172 +++++++++ .../Screens/X11Screens.Scaling.cs | 249 +++++++++++++ src/Avalonia.X11/Screens/X11Screens.cs | 93 +++++ src/Avalonia.X11/TransparencyHelper.cs | 12 +- src/Avalonia.X11/X11Globals.cs | 29 +- src/Avalonia.X11/X11Platform.cs | 8 +- src/Avalonia.X11/X11Screens.cs | 337 ------------------ src/Avalonia.X11/X11Window.cs | 13 +- src/Avalonia.X11/XResources.cs | 75 ++++ 9 files changed, 623 insertions(+), 365 deletions(-) create mode 100644 src/Avalonia.X11/Screens/X11Screen.Providers.cs create mode 100644 src/Avalonia.X11/Screens/X11Screens.Scaling.cs create mode 100644 src/Avalonia.X11/Screens/X11Screens.cs delete mode 100644 src/Avalonia.X11/X11Screens.cs create mode 100644 src/Avalonia.X11/XResources.cs diff --git a/src/Avalonia.X11/Screens/X11Screen.Providers.cs b/src/Avalonia.X11/Screens/X11Screen.Providers.cs new file mode 100644 index 0000000000..7ba3c67fa6 --- /dev/null +++ b/src/Avalonia.X11/Screens/X11Screen.Providers.cs @@ -0,0 +1,172 @@ + +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Platform; +using static Avalonia.X11.XLib; +namespace Avalonia.X11.Screens; + +internal partial class X11Screens +{ + internal class X11Screen + { + public bool IsPrimary { get; } + public string Name { get; set; } + public PixelRect Bounds { get; set; } + public Size? PhysicalSize { get; set; } + public PixelRect WorkingArea { get; set; } + + public X11Screen( + PixelRect bounds, + bool isPrimary, + string name, + Size? physicalSize) + { + IsPrimary = isPrimary; + Name = name; + Bounds = bounds; + PhysicalSize = physicalSize; + } + } + + internal interface IX11RawScreenInfoProvider + { + X11Screen[] Screens { get; } + event Action Changed; + } + + + private class Randr15ScreensImpl : IX11RawScreenInfoProvider + { + private X11Screen[] _cache; + private readonly X11Info _x11; + private readonly IntPtr _window; + + // Length of a EDID-Block-Length(128 bytes), XRRGetOutputProperty multiplies offset and length by 4 + private const int EDIDStructureLength = 32; + + public event Action Changed; + + public Randr15ScreensImpl(AvaloniaX11Platform platform) + { + _x11 = platform.Info; + _window = CreateEventWindow(platform, OnEvent); + XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify); + } + + private void OnEvent(ref XEvent ev) + { + if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify) + { + _cache = null; + Changed?.Invoke(); + } + } + + private unsafe Size? GetPhysicalMonitorSizeFromEDID(IntPtr rrOutput) + { + if (rrOutput == IntPtr.Zero) + return null; + var properties = XRRListOutputProperties(_x11.Display, rrOutput, out int propertyCount); + var hasEDID = false; + for (var pc = 0; pc < propertyCount; pc++) + { + if (properties[pc] == _x11.Atoms.EDID) + hasEDID = true; + } + + if (!hasEDID) + return null; + XRRGetOutputProperty(_x11.Display, rrOutput, _x11.Atoms.EDID, 0, EDIDStructureLength, false, false, + _x11.Atoms.AnyPropertyType, out IntPtr actualType, out int actualFormat, out int bytesAfter, out _, + out IntPtr prop); + if (actualType != _x11.Atoms.XA_INTEGER) + return null; + if (actualFormat != 8) // Expecting an byte array + return null; + + var edid = new byte[bytesAfter]; + Marshal.Copy(prop, edid, 0, bytesAfter); + XFree(prop); + XFree(new IntPtr(properties)); + if (edid.Length < 22) + return null; + var width = edid[21]; // 0x15 1 Max. Horizontal Image Size cm. + var height = edid[22]; // 0x16 1 Max. Vertical Image Size cm. + if (width == 0 && height == 0) + return null; + return new Size(width * 10, height * 10); + } + + public unsafe X11Screen[] Screens + { + get + { + if (_cache != null) + return _cache; + var monitors = XRRGetMonitors(_x11.Display, _window, true, out var count); + + var screens = new X11Screen[count]; + for (var c = 0; c < count; c++) + { + var mon = monitors[c]; + var namePtr = XGetAtomName(_x11.Display, mon.Name); + var name = Marshal.PtrToStringAnsi(namePtr); + XFree(namePtr); + var bounds = new PixelRect(mon.X, mon.Y, mon.Width, mon.Height); + Size? pSize = null; + + for (int o = 0; o < mon.NOutput; o++) + { + var outputSize = GetPhysicalMonitorSizeFromEDID(mon.Outputs[o]); + if (outputSize != null) + { + pSize = outputSize; + break; + } + } + + screens[c] = new X11Screen(bounds, mon.Primary != 0, name, pSize); + } + + XFree(new IntPtr(monitors)); + _cache = UpdateWorkArea(_x11, screens); + return screens; + } + } + } + + private class FallbackScreensImpl : IX11RawScreenInfoProvider + { + private readonly X11Info _info; + public event Action? Changed; + + public FallbackScreensImpl(AvaloniaX11Platform platform) + { + _info = platform.Info; + if (UpdateRootWindowGeometry()) + platform.Globals.RootGeometryChangedChanged += () => UpdateRootWindowGeometry(); + } + + bool UpdateRootWindowGeometry() + { + var res = XGetGeometry(_info.Display, _info.RootWindow, out var geo); + if(res) + { + Screens = UpdateWorkArea(_info, + new[] + { + new X11Screen(new PixelRect(0, 0, geo.width, geo.height), true, "Default", null) + }); + } + + return res; + } + + public X11Screen[] Screens { get; private set; } = new[] + { new X11Screen(new PixelRect(0, 0, 1920, 1280), true, "Default", null) }; + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/Screens/X11Screens.Scaling.cs b/src/Avalonia.X11/Screens/X11Screens.Scaling.cs new file mode 100644 index 0000000000..c57789fcbe --- /dev/null +++ b/src/Avalonia.X11/Screens/X11Screens.Scaling.cs @@ -0,0 +1,249 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Avalonia.X11.Screens; + +internal partial class X11Screens +{ + interface IScalingProvider + { + double GetScaling(X11Screen screen, int index); + } + + interface IScalingProviderWithChanges : IScalingProvider + { + event Action SettingsChanged; + } + + class PostMultiplyScalingProvider : IScalingProvider + { + private readonly IScalingProvider _inner; + private readonly double _factor; + + public PostMultiplyScalingProvider(IScalingProvider inner, double factor) + { + _inner = inner; + _factor = factor; + } + + public double GetScaling(X11Screen screen, int index) => _inner.GetScaling(screen, index) * _factor; + } + + class NullScalingProvider : IScalingProvider + { + public double GetScaling(X11Screen screen, int index) => 1; + } + + + class XrdbScalingProvider : IScalingProviderWithChanges + { + private readonly XResources _resources; + private double _factor = 1; + + public XrdbScalingProvider(AvaloniaX11Platform platform) + { + _resources = platform.Resources; + _resources.ResourceChanged += name => + { + if (name == "Xft.dpi") + Update(); + }; + Update(); + } + + void Update() + { + var factor = 1d; + var stringValue = _resources.GetResource("Xft.dpi")?.Trim(); + if (!string.IsNullOrWhiteSpace(stringValue) && double.TryParse(stringValue, NumberStyles.Any, + CultureInfo.InvariantCulture, out var parsed)) + { + factor = parsed / 96; + } + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (_factor != factor) + { + _factor = factor; + SettingsChanged?.Invoke(); + } + } + + public event Action? SettingsChanged; + + public double GetScaling(X11Screen screen, int index) => _factor; + } + + class PhysicalDpiScalingProvider : IScalingProvider + { + private const int FullHDWidth = 1920; + private const int FullHDHeight = 1080; + + public double GetScaling(X11Screen screen, int index) + { + if (screen.PhysicalSize == null) + return 1; + return GuessPixelDensity(screen.Bounds, screen.PhysicalSize.Value); + } + + double GuessPixelDensity(PixelRect pixel, Size physical) + { + var calculatedDensity = 1d; + if (physical.Width > 0) + calculatedDensity = pixel.Width <= FullHDWidth + ? 1 + : Math.Max(1, pixel.Width / physical.Width * 25.4 / 96); + else if (physical.Height > 0) + calculatedDensity = pixel.Height <= FullHDHeight + ? 1 + : Math.Max(1, pixel.Height / physical.Height * 25.4 / 96); + + if (calculatedDensity > 3) + return 1; + else + { + var sanePixelDensities = new double[] { 1, 1.25, 1.50, 1.75, 2 }; + foreach (var saneDensity in sanePixelDensities) + { + if (calculatedDensity <= saneDensity + 0.20) + return saneDensity; + } + + return sanePixelDensities.Last(); + } + } + } + + class UserConfiguredScalingProvider : IScalingProvider + { + private readonly Dictionary? _namedConfig; + private readonly List? _indexedConfig; + + + public UserConfiguredScalingProvider(Dictionary? namedConfig, List? indexedConfig) + { + _namedConfig = namedConfig; + _indexedConfig = indexedConfig; + } + + public double GetScaling(X11Screen screen, int index) + { + if (_indexedConfig != null) + { + if (index > 0 && index < _indexedConfig.Count) + return _indexedConfig[index]; + return 1; + } + if (_namedConfig?.TryGetValue(screen.Name, out var scaling) == true) + return scaling; + + return 1; + } + } + + class UserScalingConfiguration + { + public Dictionary? NamedConfig { get; set; } + public List? IndexedConfig { get; set; } + } + + static (UserScalingConfiguration? config, double global, bool forceAuto)? TryGetEnvConfiguration( + string globalFactorName, string userConfigName, string[] autoNames) + { + var globalFactorString = Environment.GetEnvironmentVariable(globalFactorName); + var screenFactorsString = Environment.GetEnvironmentVariable(userConfigName); + bool usePhysicalDpi = false; + foreach (var autoName in autoNames) + { + var envValue = Environment.GetEnvironmentVariable(autoName); + if (envValue == "1") + usePhysicalDpi = true; + } + + double? globalFactor = null; + if (!string.IsNullOrWhiteSpace(globalFactorString) + && double.TryParse(globalFactorString, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsed)) + globalFactor = parsed; + + UserScalingConfiguration? userConfig = null; + if (!string.IsNullOrWhiteSpace(screenFactorsString)) + { + try + { + var split = screenFactorsString.Split(';').Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + if (split[0].Contains("=")) + { + userConfig = new UserScalingConfiguration + { + NamedConfig = split.Select(x => x.Split(new[] { '=' }, 2)) + .ToDictionary(x => x[0], x => double.Parse(x[1], CultureInfo.InvariantCulture)) + }; + } + else + { + userConfig = new UserScalingConfiguration + { + IndexedConfig = split.Select(x => double.Parse(x, CultureInfo.InvariantCulture)).ToList() + }; + } + } + catch + { + Console.Error.WriteLine($"Unable to parse {userConfigName}={screenFactorsString}"); + } + } + + + if (globalFactorString == null && screenFactorsString == null && usePhysicalDpi == null) + return null; + + return (userConfig, globalFactor ?? 1, usePhysicalDpi); + } + + + static IScalingProvider GetScalingProvider(AvaloniaX11Platform platform) + { + var envSets = new[] + { + ("AVALONIA_GLOBAL_SCALE_FACTOR", "AVALONIA_SCREEN_SCALE_FACTORS", new[] { "AVALONIA_USE_PHYSICAL_DPI" }) + }.ToList(); + + if (Environment.GetEnvironmentVariable("AVALONIA_SCREEN_SCALE_IGNORE_QT") != "1") + { + envSets.Add(("QT_SCALE_FACTOR", "QT_SCREEN_SCALE_FACTORS", + new[] { "QT_AUTO_SCREEN_SCALE_FACTOR", "QT_USE_PHYSICAL_DPI" })); + } + + UserScalingConfiguration? config = null; + double global = 1; + bool forceAuto = false; + + + foreach (var envSet in envSets) + { + var envConfig = TryGetEnvConfiguration(envSet.Item1, envSet.Item2, envSet.Item3); + if (envConfig != null) + { + (config, global, forceAuto) = envConfig.Value; + break; + } + } + + IScalingProvider provider; + if (config != null) + provider = new UserConfiguredScalingProvider(config.NamedConfig, config.IndexedConfig); + else if (forceAuto) + provider = new PhysicalDpiScalingProvider(); + else + provider = new XrdbScalingProvider(platform); + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (global != 1) + provider = new PostMultiplyScalingProvider(provider, global); + + return provider; + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/Screens/X11Screens.cs b/src/Avalonia.X11/Screens/X11Screens.cs new file mode 100644 index 0000000000..bced3b669c --- /dev/null +++ b/src/Avalonia.X11/Screens/X11Screens.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Platform; +using static Avalonia.X11.XLib; + +namespace Avalonia.X11.Screens +{ + internal partial class X11Screens : IScreenImpl + { + private IX11RawScreenInfoProvider _impl; + private IScalingProvider _scaling; + internal event Action Changed; + + public X11Screens(AvaloniaX11Platform platform) + { + var info = platform.Info; + _impl = (info.RandrVersion != null && info.RandrVersion >= new Version(1, 5)) + ? new Randr15ScreensImpl(platform) + : (IX11RawScreenInfoProvider)new FallbackScreensImpl(platform); + _impl.Changed += () => Changed?.Invoke(); + _scaling = GetScalingProvider(platform); + if (_scaling is IScalingProviderWithChanges scalingWithChanges) + scalingWithChanges.SettingsChanged += () => Changed?.Invoke(); + } + + private static unsafe X11Screen[] UpdateWorkArea(X11Info info, X11Screen[] screens) + { + var rect = default(PixelRect); + foreach (var s in screens) + { + rect = rect.Union(s.Bounds); + //Fallback value + s.WorkingArea = s.Bounds; + } + + var res = XGetWindowProperty(info.Display, + info.RootWindow, + info.Atoms._NET_WORKAREA, + IntPtr.Zero, + new IntPtr(128), + false, + info.Atoms.AnyPropertyType, + out var type, + out var format, + out var count, + out var bytesAfter, + out var prop); + + if (res != (int)Status.Success || type == IntPtr.Zero || + format == 0 || bytesAfter.ToInt64() != 0 || count.ToInt64() % 4 != 0) + return screens; + + var pwa = (IntPtr*)prop; + var wa = new PixelRect(pwa[0].ToInt32(), pwa[1].ToInt32(), pwa[2].ToInt32(), pwa[3].ToInt32()); + + + foreach (var s in screens) + { + s.WorkingArea = s.Bounds.Intersect(wa); + if (s.WorkingArea.Width <= 0 || s.WorkingArea.Height <= 0) + s.WorkingArea = s.Bounds; + } + + XFree(prop); + return screens; + } + + + public Screen ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } + + public int ScreenCount => _impl.Screens.Length; + + public IReadOnlyList AllScreens => + _impl.Screens.Select((s, i) => new Screen(_scaling.GetScaling(s, i), s.Bounds, s.WorkingArea, s.IsPrimary)) + .ToArray(); + } +} diff --git a/src/Avalonia.X11/TransparencyHelper.cs b/src/Avalonia.X11/TransparencyHelper.cs index 3cc901e332..dd70643cfd 100644 --- a/src/Avalonia.X11/TransparencyHelper.cs +++ b/src/Avalonia.X11/TransparencyHelper.cs @@ -6,7 +6,7 @@ using Avalonia.Controls; namespace Avalonia.X11 { - internal class TransparencyHelper : IDisposable, X11Globals.IGlobalsSubscriber + internal class TransparencyHelper : IDisposable { private readonly X11Info _x11; private readonly IntPtr _window; @@ -35,7 +35,8 @@ namespace Avalonia.X11 _x11 = x11; _window = window; _globals = globals; - _globals.AddSubscriber(this); + _globals.CompositionChanged += UpdateTransparency; + _globals.WindowManagerChanged += UpdateTransparency; } public void SetTransparencyRequest(IReadOnlyList levels) @@ -106,11 +107,8 @@ namespace Avalonia.X11 public void Dispose() { - _globals.RemoveSubscriber(this); + _globals.WindowManagerChanged -= UpdateTransparency; + _globals.CompositionChanged -= UpdateTransparency; } - - void X11Globals.IGlobalsSubscriber.WmChanged(string wmName) => UpdateTransparency(); - - void X11Globals.IGlobalsSubscriber.CompositionChanged(bool compositing) => UpdateTransparency(); } } diff --git a/src/Avalonia.X11/X11Globals.cs b/src/Avalonia.X11/X11Globals.cs index 1ae0544724..80e1eebd16 100644 --- a/src/Avalonia.X11/X11Globals.cs +++ b/src/Avalonia.X11/X11Globals.cs @@ -12,12 +12,16 @@ namespace Avalonia.X11 private readonly X11Info _x11; private readonly IntPtr _rootWindow; private readonly IntPtr _compositingAtom; - private readonly List _subscribers = new List(); private string _wmName; private IntPtr _compositionAtomOwner; private bool _isCompositionEnabled; + public event Action WindowManagerChanged; + public event Action CompositionChanged; + public event Action RootPropertyChanged; + public event Action RootGeometryChangedChanged; + public X11Globals(AvaloniaX11Platform plat) { _plat = plat; @@ -40,9 +44,7 @@ namespace Avalonia.X11 if (_wmName != value) { _wmName = value; - // The collection might change during enumeration - foreach (var s in _subscribers.ToArray()) - s.WmChanged(value); + WindowManagerChanged?.Invoke(); } } } @@ -68,9 +70,7 @@ namespace Avalonia.X11 if (_isCompositionEnabled != value) { _isCompositionEnabled = value; - // The collection might change during enumeration - foreach (var s in _subscribers.ToArray()) - s.CompositionChanged(value); + CompositionChanged?.Invoke(); } } } @@ -160,6 +160,12 @@ namespace Avalonia.X11 { if(ev.PropertyEvent.atom == _x11.Atoms._NET_SUPPORTING_WM_CHECK) UpdateWmName(); + RootPropertyChanged?.Invoke(ev.PropertyEvent.atom); + } + + if (ev.type == XEventName.ConfigureNotify) + { + RootGeometryChangedChanged?.Invoke(); } if (ev.type == XEventName.ClientMessage) @@ -169,14 +175,5 @@ namespace Avalonia.X11 UpdateCompositingAtomOwner(); } } - - public interface IGlobalsSubscriber - { - void WmChanged(string wmName); - void CompositionChanged(bool compositing); - } - - public void AddSubscriber(IGlobalsSubscriber subscriber) => _subscribers.Add(subscriber); - public void RemoveSubscriber(IGlobalsSubscriber subscriber) => _subscribers.Remove(subscriber); } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 91c0190e8b..cd667e6c01 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -17,6 +17,7 @@ using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11; using Avalonia.X11.Glx; +using Avalonia.X11.Screens; using static Avalonia.X11.XLib; namespace Avalonia.X11 @@ -29,12 +30,13 @@ namespace Avalonia.X11 new Dictionary(); public XI2Manager XI2; public X11Info Info { get; private set; } - public IX11Screens X11Screens { get; private set; } + public X11Screens X11Screens { get; private set; } public Compositor Compositor { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } public X11Globals Globals { get; private set; } + public XResources Resources { get; private set; } public ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue { get; } = new(); public void Initialize(X11PlatformOptions options) @@ -63,6 +65,7 @@ namespace Avalonia.X11 Info = new X11Info(Display, DeferredDisplay, useXim); Globals = new X11Globals(this); + Resources = new XResources(this); //TODO: log if (options.UseDBusMenu) DBusHelper.TryInitialize(); @@ -80,8 +83,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) .Bind().ToConstant(new X11PlatformLifetimeEvents(this)); - X11Screens = X11.X11Screens.Init(this); - Screens = new X11Screens(X11Screens); + Screens = X11Screens = new X11Screens(this); if (Info.XInputVersion != null) { var xi2 = new XI2Manager(); diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs deleted file mode 100644 index 6c7283952d..0000000000 --- a/src/Avalonia.X11/X11Screens.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Runtime.InteropServices; -using Avalonia.Platform; -using static Avalonia.X11.XLib; - -namespace Avalonia.X11 -{ - internal class X11Screens : IScreenImpl - { - private IX11Screens _impl; - - public X11Screens(IX11Screens impl) - { - _impl = impl; - } - - private static unsafe X11Screen[] UpdateWorkArea(X11Info info, X11Screen[] screens) - { - var rect = default(PixelRect); - foreach (var s in screens) - { - rect = rect.Union(s.Bounds); - //Fallback value - s.WorkingArea = s.Bounds; - } - - var res = XGetWindowProperty(info.Display, - info.RootWindow, - info.Atoms._NET_WORKAREA, - IntPtr.Zero, - new IntPtr(128), - false, - info.Atoms.AnyPropertyType, - out var type, - out var format, - out var count, - out var bytesAfter, - out var prop); - - if (res != (int)Status.Success || type == IntPtr.Zero || - format == 0 || bytesAfter.ToInt64() != 0 || count.ToInt64() % 4 != 0) - return screens; - - var pwa = (IntPtr*)prop; - var wa = new PixelRect(pwa[0].ToInt32(), pwa[1].ToInt32(), pwa[2].ToInt32(), pwa[3].ToInt32()); - - - foreach (var s in screens) - { - s.WorkingArea = s.Bounds.Intersect(wa); - if (s.WorkingArea.Width <= 0 || s.WorkingArea.Height <= 0) - s.WorkingArea = s.Bounds; - } - - XFree(prop); - return screens; - } - - private class Randr15ScreensImpl : IX11Screens - { - private readonly X11ScreensUserSettings _settings; - private X11Screen[] _cache; - private X11Info _x11; - private IntPtr _window; - private const int EDIDStructureLength = 32; // Length of a EDID-Block-Length(128 bytes), XRRGetOutputProperty multiplies offset and length by 4 - - public Randr15ScreensImpl(AvaloniaX11Platform platform, X11ScreensUserSettings settings) - { - _settings = settings; - _x11 = platform.Info; - _window = CreateEventWindow(platform, OnEvent); - XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify); - } - - private void OnEvent(ref XEvent ev) - { - // Invalidate cache on RRScreenChangeNotify - if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify) - _cache = null; - } - - private unsafe Size? GetPhysicalMonitorSizeFromEDID(IntPtr rrOutput) - { - if(rrOutput == IntPtr.Zero) - return null; - var properties = XRRListOutputProperties(_x11.Display,rrOutput, out int propertyCount); - var hasEDID = false; - for(var pc = 0; pc < propertyCount; pc++) - { - if(properties[pc] == _x11.Atoms.EDID) - hasEDID = true; - } - if(!hasEDID) - return null; - XRRGetOutputProperty(_x11.Display, rrOutput, _x11.Atoms.EDID, 0, EDIDStructureLength, false, false, _x11.Atoms.AnyPropertyType, out IntPtr actualType, out int actualFormat, out int bytesAfter, out _, out IntPtr prop); - if(actualType != _x11.Atoms.XA_INTEGER) - return null; - if(actualFormat != 8) // Expecting an byte array - return null; - - var edid = new byte[bytesAfter]; - Marshal.Copy(prop,edid,0,bytesAfter); - XFree(prop); - XFree(new IntPtr(properties)); - if(edid.Length < 22) - return null; - var width = edid[21]; // 0x15 1 Max. Horizontal Image Size cm. - var height = edid[22]; // 0x16 1 Max. Vertical Image Size cm. - if(width == 0 && height == 0) - return null; - return new Size(width * 10, height * 10); - } - - public unsafe X11Screen[] Screens - { - get - { - if (_cache != null) - return _cache; - var monitors = XRRGetMonitors(_x11.Display, _window, true, out var count); - - var screens = new X11Screen[count]; - for (var c = 0; c < count; c++) - { - var mon = monitors[c]; - var namePtr = XGetAtomName(_x11.Display, mon.Name); - var name = Marshal.PtrToStringAnsi(namePtr); - XFree(namePtr); - var bounds = new PixelRect(mon.X, mon.Y, mon.Width, mon.Height); - Size? pSize = null; - double density = 0; - if (_settings.NamedScaleFactors?.TryGetValue(name, out density) != true) - { - for(int o = 0; o < mon.NOutput; o++) - { - var outputSize = GetPhysicalMonitorSizeFromEDID(mon.Outputs[o]); - var outputDensity = 1d; - if(outputSize != null) - outputDensity = X11Screen.GuessPixelDensity(bounds, outputSize.Value); - if(density == 0 || density > outputDensity) - { - density = outputDensity; - pSize = outputSize; - } - } - } - if(density == 0) - density = 1; - density *= _settings.GlobalScaleFactor; - screens[c] = new X11Screen(bounds, mon.Primary != 0, name, pSize, density); - } - - XFree(new IntPtr(monitors)); - _cache = UpdateWorkArea(_x11, screens); - return screens; - } - } - } - - private class FallbackScreensImpl : IX11Screens - { - public FallbackScreensImpl(X11Info info, X11ScreensUserSettings settings) - { - if (XGetGeometry(info.Display, info.RootWindow, out var geo)) - { - - Screens = UpdateWorkArea(info, - new[] - { - new X11Screen(new PixelRect(0, 0, geo.width, geo.height), true, "Default", null, - settings.GlobalScaleFactor) - }); - } - else - { - Screens = new[] - { - new X11Screen(new PixelRect(0, 0, 1920, 1280), true, "Default", null, - settings.GlobalScaleFactor) - }; - } - } - - public X11Screen[] Screens { get; } - } - - public static IX11Screens Init(AvaloniaX11Platform platform) - { - var info = platform.Info; - var settings = X11ScreensUserSettings.Detect(); - var impl = (info.RandrVersion != null && info.RandrVersion >= new Version(1, 5)) - ? new Randr15ScreensImpl(platform, settings) - : (IX11Screens)new FallbackScreensImpl(info, settings); - - return impl; - - } - - public Screen ScreenFromPoint(PixelPoint point) - { - return ScreenHelper.ScreenFromPoint(point, AllScreens); - } - - public Screen ScreenFromRect(PixelRect rect) - { - return ScreenHelper.ScreenFromRect(rect, AllScreens); - } - - public Screen ScreenFromWindow(IWindowBaseImpl window) - { - return ScreenHelper.ScreenFromWindow(window, AllScreens); - } - - public int ScreenCount => _impl.Screens.Length; - - public IReadOnlyList AllScreens => - _impl.Screens.Select(s => new Screen(s.Scaling, s.Bounds, s.WorkingArea, s.IsPrimary)).ToArray(); - } - - internal interface IX11Screens - { - X11Screen[] Screens { get; } - } - - internal class X11ScreensUserSettings - { - public double GlobalScaleFactor { get; set; } = 1; - public Dictionary NamedScaleFactors { get; set; } - - private static double? TryParse(string s) - { - if (s == null) - return null; - if (double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var rv)) - return rv; - return null; - } - - - public static X11ScreensUserSettings DetectEnvironment() - { - var globalFactor = Environment.GetEnvironmentVariable("AVALONIA_GLOBAL_SCALE_FACTOR"); - var screenFactors = Environment.GetEnvironmentVariable("AVALONIA_SCREEN_SCALE_FACTORS"); - if (globalFactor == null && screenFactors == null) - return null; - - var rv = new X11ScreensUserSettings - { - GlobalScaleFactor = TryParse(globalFactor) ?? 1 - }; - - try - { - if (!string.IsNullOrWhiteSpace(screenFactors)) - { - rv.NamedScaleFactors = screenFactors.Split(';').Where(x => !string.IsNullOrWhiteSpace(x)) - .Select(x => x.Split('=')).ToDictionary(x => x[0], - x => double.Parse(x[1], CultureInfo.InvariantCulture)); - } - } - catch - { - //Ignore - } - - return rv; - } - - - public static X11ScreensUserSettings Detect() - { - return DetectEnvironment() ?? new X11ScreensUserSettings(); - } - } - - internal class X11Screen - { - private const int FullHDWidth = 1920; - private const int FullHDHeight = 1080; - public bool IsPrimary { get; } - public string Name { get; set; } - public PixelRect Bounds { get; set; } - public Size? PhysicalSize { get; set; } - public double Scaling { get; set; } - public PixelRect WorkingArea { get; set; } - - public X11Screen( - PixelRect bounds, - bool isPrimary, - string name, - Size? physicalSize, - double? scaling) - { - IsPrimary = isPrimary; - Name = name; - Bounds = bounds; - if (physicalSize == null && scaling == null) - { - Scaling = 1; - } - else if (scaling == null) - { - Scaling = GuessPixelDensity(bounds, physicalSize.Value); - } - else - { - Scaling = scaling.Value; - PhysicalSize = physicalSize; - } - } - - public static double GuessPixelDensity(PixelRect pixel, Size physical) - { - var calculatedDensity = 1d; - if(physical.Width > 0) - calculatedDensity = pixel.Width <= FullHDWidth ? 1 : Math.Max(1, pixel.Width / physical.Width * 25.4 / 96); - else if(physical.Height > 0) - calculatedDensity = pixel.Height <= FullHDHeight ? 1 : Math.Max(1, pixel.Height / physical.Height * 25.4 / 96); - - if(calculatedDensity > 3) - return 1; - else - { - var sanePixelDensities = new double[] { 1, 1.25, 1.50, 1.75, 2 }; - foreach(var saneDensity in sanePixelDensities) - { - if(calculatedDensity <= saneDensity + 0.20) - return saneDensity; - } - return sanePixelDensities.Last(); - } - } - } -} diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 1bdeb91f0c..9a0067c279 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -230,6 +230,8 @@ namespace Avalonia.X11 () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult(null), () => GtkSystemDialog.TryCreate(this) }); + + platform.X11Screens.Changed += OnScreensChanged; } private class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -585,6 +587,11 @@ namespace Avalonia.X11 return extents; } + private void OnScreensChanged() + { + UpdateScaling(); + } + private bool UpdateScaling(bool skipResize = false) { double newScaling; @@ -592,7 +599,7 @@ namespace Avalonia.X11 newScaling = _scalingOverride.Value; else { - var monitor = _platform.X11Screens.Screens.OrderBy(x => x.Scaling) + var monitor = _platform.X11Screens.AllScreens.OrderBy(x => x.Scaling) .FirstOrDefault(m => m.Bounds.Contains(_position ?? default)); newScaling = monitor?.Scaling ?? RenderScaling; } @@ -926,6 +933,8 @@ namespace Avalonia.X11 if (!fromDestroyNotification) XDestroyWindow(_x11.Display, handle); } + + _platform.X11Screens.Changed -= OnScreensChanged; if (_useRenderWindow && _renderHandle != IntPtr.Zero) { @@ -1095,7 +1104,7 @@ namespace Avalonia.X11 public IScreenImpl Screen => _platform.Screens; - public Size MaxAutoSizeHint => _platform.X11Screens.Screens.Select(s => s.Bounds.Size.ToSize(s.Scaling)) + public Size MaxAutoSizeHint => _platform.X11Screens.AllScreens.Select(s => s.Bounds.Size.ToSize(s.Scaling)) .OrderByDescending(x => x.Width + x.Height).FirstOrDefault(); diff --git a/src/Avalonia.X11/XResources.cs b/src/Avalonia.X11/XResources.cs new file mode 100644 index 0000000000..a4e832cb60 --- /dev/null +++ b/src/Avalonia.X11/XResources.cs @@ -0,0 +1,75 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using static Avalonia.X11.XLib; +namespace Avalonia.X11; + +internal class XResources +{ + private Dictionary _resources = new(); + private readonly X11Info _x11; + public event Action? ResourceChanged; + + public XResources(AvaloniaX11Platform plat) + { + _x11 = plat.Info; + plat.Globals.RootPropertyChanged += OnRootPropertyChanged; + UpdateResources(); + } + + void UpdateResources() + { + var res = ReadResourcesString() ?? ""; + var items = res.Split('\n'); + var newResources = new Dictionary(); + var missingResources = new HashSet(_resources.Keys); + var changedResources = new HashSet(); + foreach (var item in items) + { + var sp = item.Split(new[] { ':' }, 2); + if (sp.Length < 2) + continue; + var key = sp[0]; + var value = sp[1].TrimStart(); + newResources[key] = value; + if (!missingResources.Remove(sp[0]) || _resources[key] != value) + changedResources.Add(key); + } + _resources = newResources; + foreach (var missing in missingResources) + ResourceChanged?.Invoke(missing); + foreach (var changed in changedResources) + ResourceChanged?.Invoke(changed); + } + + public string? GetResource(string key) + { + _resources.TryGetValue(key, out var value); + return value; + } + + string ReadResourcesString() + { + XGetWindowProperty(_x11.Display, _x11.RootWindow, _x11.Atoms.XA_RESOURCE_MANAGER, + IntPtr.Zero, new IntPtr(0x7fffffff), + false, _x11.Atoms.XA_STRING, out var actualType, out var actualFormat, + out var nitems, out _, out var prop); + try + { + if (actualFormat != 8) + return null; + return Marshal.PtrToStringAnsi(prop, nitems.ToInt32()); + } + finally + { + XFree(prop); + } + } + + private void OnRootPropertyChanged(IntPtr atom) + { + if (atom == _x11.Atoms.XA_RESOURCE_MANAGER) + UpdateResources(); + } +} \ No newline at end of file From 7b67fda0ebf8526b0c278d2d9d6568ca97e07d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihnea=20R=C4=83dulescu?= <29178174+mihnea-radulescu@users.noreply.github.com> Date: Fri, 15 Sep 2023 00:25:37 +0200 Subject: [PATCH 03/18] Fix issue 4427 - System.InvalidOperationException: Default font family name can't be null or empty (#12817) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix issue 4427 - System.InvalidOperationException: Default font family name can't be null or empty * Updated FontManager * Removed Linq usage from FontManager --------- Co-authored-by: Mihnea Rădulescu <> Co-authored-by: Benedikt Stebner --- src/Avalonia.Base/Media/FontManager.cs | 40 ++++++++----- .../HeadlessPlatformStubs.cs | 59 +++++++++++++++++++ .../Media/FontManagerTests.cs | 19 +++++- 3 files changed, 102 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 850d0011f5..af7a58dbe1 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -28,20 +28,13 @@ namespace Avalonia.Media { PlatformImpl = platformImpl; - var options = AvaloniaLocator.Current.GetService(); + AddFontCollection(new SystemFontCollection(this)); + var options = AvaloniaLocator.Current.GetService(); _fontFallbacks = options?.FontFallbacks; - var defaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName(); - - if (string.IsNullOrEmpty(defaultFontFamilyName)) - { - throw new InvalidOperationException("Default font family name can't be null or empty."); - } - + var defaultFontFamilyName = GetDefaultFontFamilyName(options); DefaultFontFamily = new FontFamily(defaultFontFamilyName); - - AddFontCollection(new SystemFontCollection(this)); } /// @@ -111,8 +104,8 @@ namespace Avalonia.Media var key = compositeKey.Keys[i]; var familyName = fontFamily.FamilyNames[i]; - - if (TryGetGlyphTypefaceByKeyAndName(typeface, key, familyName, out glyphTypeface) && + + if (TryGetGlyphTypefaceByKeyAndName(typeface, key, familyName, out glyphTypeface) && glyphTypeface.FamilyName.Contains(familyName)) { return true; @@ -165,7 +158,7 @@ namespace Avalonia.Media source = new Uri(key.BaseUri, source); } - if (TryGetFontCollection(source, out var fontCollection) && + if (TryGetFontCollection(source, out var fontCollection) && fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { if (glyphTypeface.FamilyName.Contains(familyName)) @@ -270,7 +263,7 @@ namespace Avalonia.Media private bool TryGetFontCollection(Uri source, [NotNullWhen(true)] out IFontCollection? fontCollection) { - if(source.Scheme == SystemFontScheme) + if (source.Scheme == SystemFontScheme) { source = SystemFontsKey; } @@ -289,5 +282,24 @@ namespace Avalonia.Media return fontCollection != null; } + + private string GetDefaultFontFamilyName(FontManagerOptions? options) + { + var defaultFontFamilyName = options?.DefaultFamilyName + ?? PlatformImpl.GetDefaultFontFamilyName(); + + if (string.IsNullOrEmpty(defaultFontFamilyName) && SystemFonts.Count > 0) + { + defaultFontFamilyName = SystemFonts[0].Name; + } + + if (string.IsNullOrEmpty(defaultFontFamilyName)) + { + throw new InvalidOperationException( + "Default font family name can't be null or empty."); + } + + return defaultFontFamilyName; + } } } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 471ea9a6d0..0311136d1c 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -250,6 +250,65 @@ namespace Avalonia.Headless } } + internal class HeadlessFontManagerWithMultipleSystemFontsStub : IFontManagerImpl + { + private readonly string[] _installedFontFamilyNames; + private readonly string _defaultFamilyName; + + public HeadlessFontManagerWithMultipleSystemFontsStub( + string[] installedFontFamilyNames, + string defaultFamilyName = "Default") + { + _installedFontFamilyNames = installedFontFamilyNames; + _defaultFamilyName = defaultFamilyName; + } + + public int TryCreateGlyphTypefaceCount { get; private set; } + + public string GetDefaultFontFamilyName() + { + return _defaultFamilyName; + } + + string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) + { + return _installedFontFamilyNames; + } + + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, + FontStretch fontStretch, + CultureInfo? culture, out Typeface fontKey) + { + fontKey = new Typeface(_defaultFamilyName); + + return false; + } + + public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + glyphTypeface = null; + + TryCreateGlyphTypefaceCount++; + + if (familyName == "Unknown") + { + return false; + } + + glyphTypeface = new HeadlessGlyphTypefaceImpl(); + + return true; + } + + public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + { + glyphTypeface = new HeadlessGlyphTypefaceImpl(); + + return true; + } + } + internal class HeadlessIconLoaderStub : IPlatformIconLoader { private class IconStub : IWindowIconImpl diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs index adb5431ce6..fe72a9dfd1 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs @@ -26,9 +26,12 @@ namespace Avalonia.Base.UnitTests.Media } [Fact] - public void Should_Throw_When_Default_FamilyName_Is_Null() + public void Should_Throw_When_Default_FamilyName_Is_Null_And_Installed_Font_Family_Names_Is_Empty() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new HeadlessFontManagerStub(null!)))) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface + .With(fontManagerImpl: new HeadlessFontManagerWithMultipleSystemFontsStub( + installedFontFamilyNames: new string[] { }, + defaultFamilyName: null)))) { Assert.Throws(() => FontManager.Current); } @@ -73,5 +76,17 @@ namespace Avalonia.Base.UnitTests.Media Assert.Equal("MyFont", typeface.FontFamily.Name); } } + + [Fact] + public void Should_Return_First_Installed_Font_Family_Name_When_Default_Family_Name_Is_Null() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface + .With(fontManagerImpl: new HeadlessFontManagerWithMultipleSystemFontsStub( + installedFontFamilyNames: new[] { "DejaVu", "Verdana" }, + defaultFamilyName: null)))) + { + Assert.Equal("DejaVu", FontManager.Current.DefaultFontFamily.Name); + } + } } } From 3619b805499a17ea3aaa99ad895804856f2c6eaf Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 15 Sep 2023 05:50:41 +0000 Subject: [PATCH 04/18] Add ThreadProxyRenderTimer (#12900) * Add ThreadProxyRenderTimer * Forgot IsBackground * Increase default stack size in ThreadProxyRenderTimer --- .../Rendering/ThreadProxyRenderTimer.cs | 70 +++++++++++++++++++ src/Avalonia.Native/AvaloniaNativePlatform.cs | 2 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs diff --git a/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs b/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs new file mode 100644 index 0000000000..0f3387cd1a --- /dev/null +++ b/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs @@ -0,0 +1,70 @@ +using System; +using System.Diagnostics; +using System.Threading; +using Avalonia.Metadata; + +namespace Avalonia.Rendering; + +[PrivateApi] +public sealed class ThreadProxyRenderTimer : IRenderTimer +{ + private readonly IRenderTimer _inner; + private readonly Stopwatch _stopwatch; + private readonly Thread _timerThread; + private readonly AutoResetEvent _autoResetEvent; + private Action? _tick; + private int _subscriberCount; + private bool _registered; + + public ThreadProxyRenderTimer(IRenderTimer inner, int maxStackSize = 1 * 1024 * 1024) + { + _inner = inner; + _stopwatch = new Stopwatch(); + _autoResetEvent = new AutoResetEvent(false); + _timerThread = new Thread(RenderTimerThreadFunc, maxStackSize) { Name = "RenderTimerLoop", IsBackground = true }; + } + + public event Action Tick + { + add + { + _tick += value; + + if (!_registered) + { + _registered = true; + _timerThread.Start(); + } + + if (_subscriberCount++ == 0) + { + _inner.Tick += InnerTick; + } + } + + remove + { + if (--_subscriberCount == 0) + { + _inner.Tick -= InnerTick; + } + + _tick -= value; + } + } + + private void RenderTimerThreadFunc() + { + while (_autoResetEvent.WaitOne()) + { + _tick?.Invoke(_stopwatch.Elapsed); + } + } + + private void InnerTick(TimeSpan obj) + { + _autoResetEvent.Set(); + } + + public bool RunsInBackground => true; +} diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index f24c6fa96f..68da780dda 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -108,7 +108,7 @@ namespace Avalonia.Native .Bind().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings())) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) - .Bind().ToConstant(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer())) + .Bind().ToConstant(new ThreadProxyRenderTimer(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer()))) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) .Bind().ToConstant(applicationPlatform) From 0b41caf4338c5a7045404988a124d691c5847abd Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 15 Sep 2023 08:53:20 +0200 Subject: [PATCH 05/18] Introduce GlyphTypeface.TryGetStream (#12696) * Introduce IGlyphTypeface2 and expose TryGetStream * Move IGlyphTypeface to its own file * Always copy the font data to a MemoryStream * Update IGlyphTypeface.cs --- src/Avalonia.Base/Media/IGlyphTypeface2.cs | 16 +++++++++++++ src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 26 ++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Base/Media/IGlyphTypeface2.cs diff --git a/src/Avalonia.Base/Media/IGlyphTypeface2.cs b/src/Avalonia.Base/Media/IGlyphTypeface2.cs new file mode 100644 index 0000000000..d0152ccb3c --- /dev/null +++ b/src/Avalonia.Base/Media/IGlyphTypeface2.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; + +namespace Avalonia.Media +{ + internal interface IGlyphTypeface2 : IGlyphTypeface + { + + /// + /// Returns the font file stream represented by the object. + /// + /// The stream. + /// Returns true if the stream can be obtained, otherwise false. + bool TryGetStream([NotNullWhen(true)] out Stream? stream); + } +} diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index 6de8cef6d1..9878270d5d 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -1,4 +1,6 @@ using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Runtime.InteropServices; using Avalonia.Media; using HarfBuzzSharp; @@ -6,7 +8,7 @@ using SkiaSharp; namespace Avalonia.Skia { - internal class GlyphTypefaceImpl : IGlyphTypeface + internal class GlyphTypefaceImpl : IGlyphTypeface, IGlyphTypeface2 { private bool _isDisposed; private readonly SKTypeface _typeface; @@ -196,5 +198,27 @@ namespace Avalonia.Skia { return _typeface.TryGetTableData(tag, out table); } + + public bool TryGetStream([NotNullWhen(true)] out Stream? stream) + { + try + { + var asset = _typeface.OpenStream(); + var size = asset.Length; + var buffer = new byte[size]; + + asset.Read(buffer, size); + + stream = new MemoryStream(buffer); + + return true; + } + catch + { + stream = null; + + return false; + } + } } } From 15ea623533c7f3fea6c6e122963ea332e52fa299 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Sep 2023 02:02:09 -0400 Subject: [PATCH 06/18] Change iOS initialization order --- src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index 18ccbad692..c1e7cd5aba 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -29,15 +29,13 @@ namespace Avalonia.iOS public bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) { var builder = AppBuilder.Configure().UseiOS(); - CustomizeAppBuilder(builder); var lifetime = new SingleViewLifetime(); builder.AfterSetup(_ => { Window = new UIWindow(); - - + var view = new AvaloniaView(); lifetime.View = view; var controller = new DefaultAvaloniaViewController @@ -47,7 +45,9 @@ namespace Avalonia.iOS Window.RootViewController = controller; view.InitWithController(controller); }); - + + CustomizeAppBuilder(builder); + builder.SetupWithLifetime(lifetime); Window.MakeKeyAndVisible(); From 5d3a90a5cc79b056511f0c807a7de9b2fac9881b Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Mon, 18 Sep 2023 05:03:45 +0200 Subject: [PATCH 07/18] Detect recursion in analyser `while` loops (#12916) Check for cancellation in analyser `while` loops --- ...valoniaPropertyAnalyzer.CompileAnalyzer.cs | 72 ++++++++++++------- .../AvaloniaPropertyAnalyzer.cs | 41 +++++++++-- 2 files changed, 83 insertions(+), 30 deletions(-) diff --git a/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index a4fa061b4b..b9de2b5f69 100644 --- a/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -122,6 +122,8 @@ public partial class AvaloniaPropertyAnalyzer while (namespaceStack.Count > 0) { + cancellationToken.ThrowIfCancellationRequested(); + var current = namespaceStack.Pop(); types.AddRange(current.GetTypeMembers()); @@ -170,7 +172,7 @@ public partial class AvaloniaPropertyAnalyzer { if (model.GetOperation(descendant, cancellationToken) is IAssignmentOperation assignmentOperation && - GetReferencedFieldOrProperty(assignmentOperation.Target) is { } target) + GetReferencedFieldOrProperty(assignmentOperation.Target, cancellationToken) is { } target) { RegisterAssignment(target, assignmentOperation.Value); } @@ -178,9 +180,10 @@ public partial class AvaloniaPropertyAnalyzer } } } - catch (Exception ex) when (ex is not OperationCanceledException) + catch (Exception ex) { - throw new AvaloniaAnalysisException($"Failed to find AvaloniaProperty objects in {type}.", ex); + WrapAndThrowIfNotCancellation(ex, $"Failed to find AvaloniaProperty objects in {type}.", cancellationToken); + throw; } }); @@ -202,14 +205,23 @@ public partial class AvaloniaPropertyAnalyzer }); // we have recorded every Register and AddOwner call. Now follow assignment chains. - Parallel.ForEach(fieldInitializations.Keys.Intersect(propertyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray(), root => + Parallel.ForEach(fieldInitializations.Keys.Intersect(propertyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray(), parallelOptions, root => { var propertyDescription = propertyDescriptions[root]; var owner = propertyDescription.AssignedTo[root]; + var seen = new HashSet(SymbolEqualityComparer.Default); + var current = root; do { + cancellationToken.ThrowIfCancellationRequested(); + + if (!seen.Add(current)) + { + break; // self-assignment, just stop processing if this happens + } + var target = fieldInitializations[current]; propertyDescription.SetAssignment(target, new(owner.Type, target.Locations[0])); // This loop handles simple assignment operations, so do NOT change the owner type @@ -225,7 +237,7 @@ public partial class AvaloniaPropertyAnalyzer var propertyDescriptionsByName = propertyDescriptions.Values.ToLookup(p => p.Name, p => (property: p, owners: p.OwnerTypes.Select(t => t.Type).ToImmutableHashSet(SymbolEqualityComparer.Default))); // Detect CLR properties that provide syntatic wrapping around an AvaloniaProperty (or potentially multiple, which leads to a warning diagnostic) - Parallel.ForEach(propertyDescriptions.Values, propertyDescription => + Parallel.ForEach(propertyDescriptions.Values, parallelOptions, propertyDescription => { var nameMatches = propertyDescriptionsByName[propertyDescription.Name]; @@ -242,6 +254,8 @@ public partial class AvaloniaPropertyAnalyzer var current = ownerType.BaseType; while (current != null) { + cancellationToken.ThrowIfCancellationRequested(); + foreach (var otherProp in nameMatches.Where(t => t.owners.Contains(current)).Select(t => t.property)) { clrPropertyWrapCandidates.Add((clrProperty, otherProp)); @@ -259,10 +273,10 @@ public partial class AvaloniaPropertyAnalyzer void RegisterAssignment(ISymbol target, IOperation value) { - switch (ResolveOperationSource(value)) + switch (ResolveOperationSource(value, cancellationToken)) { case IInvocationOperation invocation: - RegisterInitializer_Invocation(invocation, target, propertyDescriptions); + RegisterInitializer_Invocation(invocation, target, propertyDescriptions, cancellationToken); break; case IFieldReferenceOperation fieldRef when IsAvaloniaPropertyStorage(fieldRef.Field): fieldInitializations[fieldRef.Field] = target; @@ -278,7 +292,7 @@ public partial class AvaloniaPropertyAnalyzer } // This method handles registration of a new AvaloniaProperty, and calls to AddOwner. - private void RegisterInitializer_Invocation(IInvocationOperation invocation, ISymbol target, ConcurrentDictionary propertyDescriptions) + private void RegisterInitializer_Invocation(IInvocationOperation invocation, ISymbol target, ConcurrentDictionary propertyDescriptions, CancellationToken cancellationToken) { try { @@ -298,7 +312,7 @@ public partial class AvaloniaPropertyAnalyzer ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam); } else if (_ownerParams.TryGetValue(originalMethod, out var ownerParam) && // try extracting the runtime argument - ResolveOperationSource(invocation.Arguments[ownerParam.Ordinal].Value) is ITypeOfOperation { Type: ITypeSymbol type } typeOf) + ResolveOperationSource(invocation.Arguments[ownerParam.Ordinal].Value, cancellationToken) is ITypeOfOperation { Type: ITypeSymbol type } typeOf) { ownerTypeRef = new TypeReference(type, typeOf.Syntax.GetLocation()); } @@ -318,7 +332,7 @@ public partial class AvaloniaPropertyAnalyzer } string name; - switch (ResolveOperationSource(invocation.Arguments[0].Value)) + switch (ResolveOperationSource(invocation.Arguments[0].Value, cancellationToken)) { case ILiteralOperation literal when SymbolEquals(literal.Type, _stringType): name = (string)literal.ConstantValue.Value!; @@ -368,7 +382,7 @@ public partial class AvaloniaPropertyAnalyzer return; } - if (GetReferencedFieldOrProperty(invocation.Instance) is not { } sourceSymbol) + if (GetReferencedFieldOrProperty(invocation.Instance, cancellationToken) is not { } sourceSymbol) { return; } @@ -411,7 +425,8 @@ public partial class AvaloniaPropertyAnalyzer } catch (Exception ex) { - throw new AvaloniaAnalysisException($"Failed to register the initializer of '{target}'.", ex); + WrapAndThrowIfNotCancellation(ex, $"Failed to register the initializer of '{target}'.", cancellationToken); + throw; } } @@ -439,7 +454,8 @@ public partial class AvaloniaPropertyAnalyzer } catch (Exception ex) { - throw new AvaloniaAnalysisException($"Failed to process initialization of field '{field}'.", ex); + WrapAndThrowIfNotCancellation(ex, $"Failed to process initialization of field '{field}'.", context.CancellationToken); + throw; } } } @@ -467,7 +483,8 @@ public partial class AvaloniaPropertyAnalyzer } catch (Exception ex) { - throw new AvaloniaAnalysisException($"Failed to process initialization of property '{property}'.", ex); + WrapAndThrowIfNotCancellation(ex, $"Failed to process initialization of property '{property}'.", context.CancellationToken); + throw; } } } @@ -479,7 +496,7 @@ public partial class AvaloniaPropertyAnalyzer try { - var (target, isValid) = ResolveOperationSource(operation.Target) switch + var (target, isValid) = ResolveOperationSource(operation.Target, context.CancellationToken) switch { IFieldReferenceOperation fieldRef => (fieldRef.Field, IsValidAvaloniaPropertyStorage(fieldRef.Field)), IPropertyReferenceOperation propertyRef => (propertyRef.Property, IsValidAvaloniaPropertyStorage(propertyRef.Property)), @@ -500,7 +517,8 @@ public partial class AvaloniaPropertyAnalyzer } catch (Exception ex) { - throw new AvaloniaAnalysisException($"Failed to process assignment '{operation}'.", ex); + WrapAndThrowIfNotCancellation(ex, $"Failed to process assignment '{operation}'.", context.CancellationToken); + throw; } } @@ -536,13 +554,13 @@ public partial class AvaloniaPropertyAnalyzer { var operation = (IAssignmentOperation)context.Operation; - if (ResolveOperationSource(operation) is IParameterReferenceOperation && context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.Constructor }) + if (ResolveOperationSource(operation, context.CancellationToken) is IParameterReferenceOperation && context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.Constructor }) { // We can consider `new MyType(myValue)` functionally equivalent to `new MyType() { Value = myValue }`. Both set a local value with an external parameter. return; } - if (ResolveOperationTarget(operation) is IPropertyReferenceOperation propertyRef && + if (ResolveOperationTarget(operation, context.CancellationToken) is IPropertyReferenceOperation propertyRef && propertyRef.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && _clrPropertyToAvaloniaProperties.TryGetValue(propertyRef.Property, out var propertyDescriptions) && propertyDescriptions.Any(p => !SymbolEquals(p.PropertyType.OriginalDefinition, _directPropertyType))) @@ -571,7 +589,7 @@ public partial class AvaloniaPropertyAnalyzer if (_allGetSetMethods.Contains(originalMethod)) { if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && - GetReferencedProperty(invocation.Arguments[0]) is { } refProp && + GetReferencedProperty(invocation.Arguments[0], context.CancellationToken) is { } refProp && refProp.description.AssignedTo.TryGetValue(refProp.storageSymbol, out var ownerType) && !DerivesFrom(context.ContainingSymbol.ContainingType, ownerType.Type) && !DerivesFrom(context.ContainingSymbol.ContainingType, refProp.description.HostType?.Type)) @@ -596,7 +614,7 @@ public partial class AvaloniaPropertyAnalyzer context.ReportDiagnostic(Diagnostic.Create(PropertyOwnedByGenericType, TypeReference.FromInvocationTypeParameter(invocation, typeParam).Location)); } - if (_avaloniaPropertyAddOwnerMethods.Contains(originalMethod) && GetReferencedProperty(invocation.Instance!) is { } refProp) + if (_avaloniaPropertyAddOwnerMethods.Contains(originalMethod) && GetReferencedProperty(invocation.Instance!, context.CancellationToken) is { } refProp) { var ownerMatches = refProp.description.AssignedTo.Where(kvp => !SymbolEquals(kvp.Key, context.ContainingSymbol) && DerivesFrom(newOwnerType, kvp.Value.Type)).ToArray(); @@ -619,7 +637,7 @@ public partial class AvaloniaPropertyAnalyzer bool IsStaticConstructorOrInitializer() => context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.StaticConstructor } || - ResolveOperationTarget(invocation.Parent!) switch + ResolveOperationTarget(invocation.Parent!, context.CancellationToken) switch { IFieldInitializerOperation fieldInit when fieldInit.InitializedFields.All(f => f.IsStatic) => true, IPropertyInitializerOperation propInit when propInit.InitializedProperties.All(p => p.IsStatic) => true, @@ -627,9 +645,9 @@ public partial class AvaloniaPropertyAnalyzer }; } - private (AvaloniaPropertyDescription description, ISymbol storageSymbol)? GetReferencedProperty(IOperation operation) + private (AvaloniaPropertyDescription description, ISymbol storageSymbol)? GetReferencedProperty(IOperation operation, CancellationToken cancellationToken) { - if (GetReferencedFieldOrProperty(operation) is { } storageSymbol && _avaloniaPropertyDescriptions.TryGetValue(storageSymbol, out var result)) + if (GetReferencedFieldOrProperty(operation, cancellationToken) is { } storageSymbol && _avaloniaPropertyDescriptions.TryGetValue(storageSymbol, out var result)) { return (result, storageSymbol); } @@ -711,7 +729,8 @@ public partial class AvaloniaPropertyAnalyzer } catch (Exception ex) { - throw new AvaloniaAnalysisException($"Failed to analyse wrapper property '{property}'.", ex); + WrapAndThrowIfNotCancellation(ex, $"Failed to analyse wrapper property '{property}'.", context.CancellationToken); + throw; } } @@ -762,7 +781,7 @@ public partial class AvaloniaPropertyAnalyzer if (operation.Arguments.Length != 0) { - switch (ResolveOperationSource(operation.Arguments[0].Value)) + switch (ResolveOperationSource(operation.Arguments[0].Value, context.CancellationToken)) { case IFieldReferenceOperation fieldRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(fieldRef.Field): case IPropertyReferenceOperation propertyRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(propertyRef.Property): @@ -793,7 +812,8 @@ public partial class AvaloniaPropertyAnalyzer } catch (Exception ex) { - throw new AvaloniaAnalysisException($"Failed to process property accessor '{method}'.", ex); + WrapAndThrowIfNotCancellation(ex, $"Failed to process property accessor '{method}'.", context.CancellationToken); + throw; } } } diff --git a/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs index 7de0e8eac4..b765a50c41 100644 --- a/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs @@ -3,8 +3,10 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using System.Runtime.Serialization; +using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; @@ -242,10 +244,20 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer /// /// Follows assignments and conversions back to their source. /// - private static IOperation ResolveOperationSource(IOperation operation) + private static IOperation ResolveOperationSource(IOperation operation, CancellationToken cancellationToken) { + var seen = new HashSet(); + while (true) { + if (!seen.Add(operation)) // https://github.com/AvaloniaUI/Avalonia/issues/12864 + { + Debug.Fail("Operation recursion detected."); + return operation; + } + + cancellationToken.ThrowIfCancellationRequested(); + switch (operation) { case IConversionOperation conversion: @@ -260,10 +272,20 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer } } - private static IOperation ResolveOperationTarget(IOperation operation) + private static IOperation ResolveOperationTarget(IOperation operation, CancellationToken cancellationToken) { + var seen = new HashSet(); + while (true) { + if (!seen.Add(operation)) // https://github.com/AvaloniaUI/Avalonia/issues/12864 + { + Debug.Fail("Operation recursion detected."); + return operation; + } + + cancellationToken.ThrowIfCancellationRequested(); + switch (operation) { case IConversionOperation conversion: @@ -278,17 +300,28 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer } } - private static ISymbol? GetReferencedFieldOrProperty(IOperation? operation) => operation == null ? null : ResolveOperationSource(operation) switch + private static ISymbol? GetReferencedFieldOrProperty(IOperation? operation, CancellationToken cancellationToken) => operation == null ? null : ResolveOperationSource(operation, cancellationToken) switch { IFieldReferenceOperation fieldRef => fieldRef.Field, IPropertyReferenceOperation propertyRef => propertyRef.Property, - IArgumentOperation argument => GetReferencedFieldOrProperty(argument.Value), + IArgumentOperation argument => GetReferencedFieldOrProperty(argument.Value, cancellationToken), _ => null, }; private static bool IsValidAvaloniaPropertyStorage(IFieldSymbol field) => field.IsStatic && field.IsReadOnly; private static bool IsValidAvaloniaPropertyStorage(IPropertySymbol field) => field.IsStatic && field.IsReadOnly; + /// + private static void WrapAndThrowIfNotCancellation(Exception exception, string analysisContextMessage, CancellationToken cancellationToken) + { + if (exception is OperationCanceledException oce && oce.CancellationToken == cancellationToken) + { + return; + } + + throw new AvaloniaAnalysisException(analysisContextMessage, exception); + } + private static bool SymbolEquals(ISymbol? x, ISymbol? y, bool includeNullability = false) { // The current version of Microsoft.CodeAnalysis includes an "IncludeNullability" comparer, From 1cb271fb372eda132bdcc76d1bd16d6256e12b16 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Mon, 18 Sep 2023 05:04:03 +0200 Subject: [PATCH 08/18] fix: Xaml Compiler error when code-behind class contains a `DllImport` method (#12882) * fix: Avalonia.Generators not found if you put SourceGenerators.props in a project with path are like this:"C:\GitHub\Avalonia\sample\mobile\android\ANoteSample\ANoteSample.csproj", the Compiler is not able to found `Avalonia.Generators` * test: Add test #10046 Xaml Compiler error when code-behind class contains a DllImport method * fix: Xaml Compiler error when code-behind class contains a DllImport method * fix: Address Review * fix: ValidateApiDiff has thrown an exception --- Avalonia.sln | 22 ++++- build/SourceGenerators.props | 2 +- .../XamlCompilerTaskExecutor.cs | 2 +- .../Avalonia.Build.Tasks.UnitTest.csproj | 39 ++++++++ .../CompileAvaloniaXamlTaskTest.cs | 38 ++++++++ .../UnitTestBuildEngine.cs | 96 +++++++++++++++++++ .../UnitTestBuildEngineMessage.cs | 39 ++++++++ tests/TestFiles/BuildTasks/PInvoke/App.axaml | 8 ++ .../TestFiles/BuildTasks/PInvoke/App.axaml.cs | 21 ++++ .../BuildTasks/PInvoke/MainWindow.axaml | 4 + .../BuildTasks/PInvoke/MainWindow.axaml.cs | 22 +++++ .../BuildTasks/PInvoke/PInvoke.csproj | 26 +++++ tests/TestFiles/BuildTasks/PInvoke/Program.cs | 14 +++ 13 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj create mode 100644 tests/Avalonia.Build.Tasks.UnitTest/CompileAvaloniaXamlTaskTest.cs create mode 100644 tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngine.cs create mode 100644 tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngineMessage.cs create mode 100644 tests/TestFiles/BuildTasks/PInvoke/App.axaml create mode 100644 tests/TestFiles/BuildTasks/PInvoke/App.axaml.cs create mode 100644 tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml create mode 100644 tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml.cs create mode 100644 tests/TestFiles/BuildTasks/PInvoke/PInvoke.csproj create mode 100644 tests/TestFiles/BuildTasks/PInvoke/Program.cs diff --git a/Avalonia.sln b/Avalonia.sln index 98ad0cadae..5ffa3dca50 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -273,7 +273,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit.Uni EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Browser", "samples\MobileSandbox.Browser\MobileSandbox.Browser.csproj", "{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Metal", "src\Avalonia.Metal\Avalonia.Metal.csproj", "{60B4ED1F-ECFA-453B-8A70-1788261C8355}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Metal", "src\Avalonia.Metal\Avalonia.Metal.csproj", "{60B4ED1F-ECFA-453B-8A70-1788261C8355}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Build.Tasks.UnitTest", "tests\Avalonia.Build.Tasks.UnitTest\Avalonia.Build.Tasks.UnitTest.csproj", "{B0FD6A48-FBAB-4676-B36A-DE76B0922B12}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestFiles", "TestFiles", "{9D6AEF22-221F-4F4B-B335-A4BA510F002C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildTasks", "BuildTasks", "{5BF0C3B8-E595-4940-AB30-2DA206C2F085}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PInvoke", "tests\TestFiles\BuildTasks\PInvoke\PInvoke.csproj", "{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -666,6 +674,14 @@ Global {60B4ED1F-ECFA-453B-8A70-1788261C8355}.Debug|Any CPU.Build.0 = Debug|Any CPU {60B4ED1F-ECFA-453B-8A70-1788261C8355}.Release|Any CPU.ActiveCfg = Release|Any CPU {60B4ED1F-ECFA-453B-8A70-1788261C8355}.Release|Any CPU.Build.0 = Release|Any CPU + {B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Release|Any CPU.Build.0 = Release|Any CPU + {0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -748,6 +764,10 @@ Global {2999D79E-3C20-4A90-B651-CA7E0AC92D35} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {B0FD6A48-FBAB-4676-B36A-DE76B0922B12} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {9D6AEF22-221F-4F4B-B335-A4BA510F002C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {5BF0C3B8-E595-4940-AB30-2DA206C2F085} = {9D6AEF22-221F-4F4B-B335-A4BA510F002C} + {0A948D71-99C5-43E9-BACB-B0BA59EA25B4} = {5BF0C3B8-E595-4940-AB30-2DA206C2F085} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props index a66bff4999..9b4d3d7cdc 100644 --- a/build/SourceGenerators.props +++ b/build/SourceGenerators.props @@ -15,7 +15,7 @@ diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 80876f1819..469a51f32a 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -484,7 +484,7 @@ namespace Avalonia.Build.Tasks var foundXamlLoader = false; // Find AvaloniaXamlLoader.Load(this) or AvaloniaXamlLoader.Load(sp, this) and replace it with !XamlIlPopulateTrampoline(this) - foreach (var method in classTypeDefinition.Methods.ToArray()) + foreach (var method in classTypeDefinition.Methods.Where(m => m.Body is not null).ToArray()) { var i = method.Body.Instructions; for (var c = 1; c < i.Count; c++) diff --git a/tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj b/tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj new file mode 100644 index 0000000000..0a289ff28b --- /dev/null +++ b/tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj @@ -0,0 +1,39 @@ + + + + net472 + false + Library + false + true + Debug + + + + + + + + + + Always + + + Always + + + + + + + + + + + + + diff --git a/tests/Avalonia.Build.Tasks.UnitTest/CompileAvaloniaXamlTaskTest.cs b/tests/Avalonia.Build.Tasks.UnitTest/CompileAvaloniaXamlTaskTest.cs new file mode 100644 index 0000000000..fee678c388 --- /dev/null +++ b/tests/Avalonia.Build.Tasks.UnitTest/CompileAvaloniaXamlTaskTest.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using System.Reflection; +using Xunit; + +namespace Avalonia.Build.Tasks.UnitTest; + +public class CompileAvaloniaXamlTaskTest +{ + + [Fact] + public void Does_Not_Fail_When_Codebehind_Contains_DllImport() + { + using var engine = UnitTestBuildEngine.Start(); + var basePath = Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath), "Assets"); + var originalAssemblyPath = Path.Combine(basePath, + "PInvoke.dll"); + var referencesPath = Path.Combine(basePath, + "PInvoke.dll.refs"); + var compiledAssemblyPath = "PInvoke.dll"; + + Assert.True(File.Exists(originalAssemblyPath), $"The original {originalAssemblyPath} don't exists."); + + new CompileAvaloniaXamlTask() + { + AssemblyFile = originalAssemblyPath, + ReferencesFilePath = referencesPath, + OutputPath = compiledAssemblyPath, + RefAssemblyFile = null, + BuildEngine = engine, + ProjectDirectory = Directory.GetCurrentDirectory(), + VerifyIl = true + }.Execute(); + Assert.Equal(0, engine.Errors.Count); + } + + +} diff --git a/tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngine.cs b/tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngine.cs new file mode 100644 index 0000000000..7dbbc7d02b --- /dev/null +++ b/tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngine.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Xunit; + +namespace Avalonia.Build.Tasks.UnitTest; + +/// +/// This is fake BuildEngine using for testing build task +/// at moment it manage only and +/// other messages are ignored/> +/// +internal class UnitTestBuildEngine : IBuildEngine, IDisposable +{ + private readonly bool _treatWarningAsError; + private readonly bool _assertOnDispose; + private readonly List _errors = new(); + + /// + /// Start new instance of + /// + /// if it is false immediately assert error + /// if it is true treat warning as error + /// if it is true assert on dispose if there are any errors. + /// + public static UnitTestBuildEngine Start(bool continueOnError = false, + bool treatWarningAsError = false, + bool assertOnDispose = false) => + new UnitTestBuildEngine(continueOnError, treatWarningAsError, assertOnDispose); + + private UnitTestBuildEngine(bool continueOnError, + bool treatWarningAsError, + bool assertOnDispose) + { + ContinueOnError = continueOnError; + _treatWarningAsError = treatWarningAsError; + _assertOnDispose = assertOnDispose; + } + + public bool ContinueOnError { get; } + + public int LineNumberOfTaskNode { get; } + + public int ColumnNumberOfTaskNode { get; } + + public string ProjectFileOfTaskNode { get; } + + public IReadOnlyList Errors => _errors; + + public bool BuildProjectFile(string projectFileName, + string[] targetNames, + IDictionary globalProperties, + IDictionary targetOutputs) + => throw new NotImplementedException(); + + public void Dispose() + { + if (_assertOnDispose && _errors.Count > 0) + { + Assert.Fail("There is one o more errors."); + } + } + + + public void LogCustomEvent(CustomBuildEventArgs e) + { + } + + public void LogMessageEvent(BuildMessageEventArgs e) + { + } + + public void LogErrorEvent(BuildErrorEventArgs e) + { + var message = UnitTestBuildEngineMessage.From(e); + _errors.Add(message); + if (!ContinueOnError) + { + Assert.Fail(message.Message); + } + } + + public void LogWarningEvent(BuildWarningEventArgs e) + { + if (_treatWarningAsError) + { + var message = UnitTestBuildEngineMessage.From(e); + _errors.Add(message); + if (!ContinueOnError) + { + Assert.Fail(message.Message); + } + } + } +} diff --git a/tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngineMessage.cs b/tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngineMessage.cs new file mode 100644 index 0000000000..280e984b28 --- /dev/null +++ b/tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngineMessage.cs @@ -0,0 +1,39 @@ +using Microsoft.Build.Framework; + +namespace Avalonia.Build.Tasks.UnitTest; + +enum MessageSource +{ + Unknown, + ErrorEvent, + MessageEvent, + CustomEvent, + WarningEvent +} + +record class UnitTestBuildEngineMessage +{ + private UnitTestBuildEngineMessage(MessageSource Type, LazyFormattedBuildEventArgs Source) + { + this.Type = Type; + this.Source = Source; + Message = Source.Message; + } + + public MessageSource Type { get; } + public LazyFormattedBuildEventArgs Source { get; } + public string Message { get; } + + public static UnitTestBuildEngineMessage From(BuildWarningEventArgs buildWarning) => + new UnitTestBuildEngineMessage(MessageSource.WarningEvent, buildWarning); + + public static UnitTestBuildEngineMessage From(BuildMessageEventArgs buildMessage) => + new UnitTestBuildEngineMessage(MessageSource.MessageEvent, buildMessage); + + public static UnitTestBuildEngineMessage From(BuildErrorEventArgs buildError) => + new UnitTestBuildEngineMessage(MessageSource.ErrorEvent, buildError); + + public static UnitTestBuildEngineMessage From(CustomBuildEventArgs customBuild) => + new UnitTestBuildEngineMessage(MessageSource.CustomEvent, customBuild); + +} diff --git a/tests/TestFiles/BuildTasks/PInvoke/App.axaml b/tests/TestFiles/BuildTasks/PInvoke/App.axaml new file mode 100644 index 0000000000..2102354427 --- /dev/null +++ b/tests/TestFiles/BuildTasks/PInvoke/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/tests/TestFiles/BuildTasks/PInvoke/App.axaml.cs b/tests/TestFiles/BuildTasks/PInvoke/App.axaml.cs new file mode 100644 index 0000000000..4f6a93c2d8 --- /dev/null +++ b/tests/TestFiles/BuildTasks/PInvoke/App.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace PInvoke; + +public class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { + desktopLifetime.MainWindow = new MainWindow(); + } + } +} diff --git a/tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml b/tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml new file mode 100644 index 0000000000..8d2a125f58 --- /dev/null +++ b/tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml @@ -0,0 +1,4 @@ + + diff --git a/tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml.cs b/tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml.cs new file mode 100644 index 0000000000..997be0988c --- /dev/null +++ b/tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace PInvoke; + +public partial class MainWindow : Window +{ + [DllImport(@"libhello")] + extern static int add(int a, int b); + + public MainWindow() + { + InitializeComponent(); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + var x = add(1, 2); + } +} diff --git a/tests/TestFiles/BuildTasks/PInvoke/PInvoke.csproj b/tests/TestFiles/BuildTasks/PInvoke/PInvoke.csproj new file mode 100644 index 0000000000..7df69b429d --- /dev/null +++ b/tests/TestFiles/BuildTasks/PInvoke/PInvoke.csproj @@ -0,0 +1,26 @@ + + + + WinExe + net6.0;netstandard2.0 + true + true + + false + true + false + + + + + + + + + + + + + + + diff --git a/tests/TestFiles/BuildTasks/PInvoke/Program.cs b/tests/TestFiles/BuildTasks/PInvoke/Program.cs new file mode 100644 index 0000000000..9c5f2b58ef --- /dev/null +++ b/tests/TestFiles/BuildTasks/PInvoke/Program.cs @@ -0,0 +1,14 @@ +using Avalonia; + +namespace PInvoke; + +public class Program +{ + static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); +} From 9eeb3ee3400e9717b92b1e740b50f9de0c222b12 Mon Sep 17 00:00:00 2001 From: DmitryZhelnin Date: Tue, 19 Sep 2023 03:52:40 +0300 Subject: [PATCH 09/18] Animatable: subscribe to collection changes only if transitions are already enabled. (#12861) Co-authored-by: Dmitry Zhelnin --- src/Avalonia.Base/Animation/Animatable.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index fb44c10f43..d67a9501da 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -123,8 +123,14 @@ namespace Avalonia.Animation toAdd = newTransitions.Except(oldTransitions).ToList(); } - newTransitions.CollectionChanged += TransitionsCollectionChangedHandler; - _isSubscribedToTransitionsCollection = true; + // Subscribe to collection changes only if transitions are already enabled, + // i.e. control is attached to the visual tree + if (_transitionsEnabled) + { + newTransitions.CollectionChanged += TransitionsCollectionChangedHandler; + _isSubscribedToTransitionsCollection = true; + } + AddTransitions(toAdd); } From 407440645ba70f1d0e09f3e120a84c43fbf72418 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 19 Sep 2023 21:02:14 +0200 Subject: [PATCH 10/18] Correctly compare source and destination pixel format (#12940) --- src/Avalonia.Base/Media/Imaging/Bitmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index 215586eef9..abf12ad686 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -241,7 +241,7 @@ namespace Avalonia.Media.Imaging throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); } - if (readable.Format != Format || readable.AlphaFormat != alphaFormat) + if (buffer.Format != readable.Format || alphaFormat != readable.AlphaFormat) { using (var fb = readable.Lock()) { From ae765e6022d4f097dd4b5dd6612f7210aa867e89 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 20 Sep 2023 00:07:36 +0300 Subject: [PATCH 11/18] [X11] Don't convert the current time from long to int (#12941) --- src/Avalonia.X11/X11PlatformThreading.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11PlatformThreading.cs b/src/Avalonia.X11/X11PlatformThreading.cs index de0e3bee5d..de494eb059 100644 --- a/src/Avalonia.X11/X11PlatformThreading.cs +++ b/src/Avalonia.X11/X11PlatformThreading.cs @@ -235,7 +235,7 @@ namespace Avalonia.X11 } - public long Now => (int)_clock.ElapsedMilliseconds; + public long Now => _clock.ElapsedMilliseconds; public bool CanQueryPendingInput => true; public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || XPending(_display) != 0; From 8f7e055392a6ea6dd115b175a1d3c5e65c14488f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 20 Sep 2023 11:16:48 +0200 Subject: [PATCH 12/18] Set _ignoreWmChar when IME sends a ImeProcessed key to prevent input via WM_CHAR (#12942) --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index f62c99ec4a..3ad4f194ad 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -775,12 +775,19 @@ namespace Avalonia.Win32 if (message == WindowsMessage.WM_KEYDOWN) { - // Handling a WM_KEYDOWN message should cause the subsequent WM_CHAR message to - // be ignored. This should be safe to do as WM_CHAR should only be produced in - // response to the call to TranslateMessage/DispatchMessage after a WM_KEYDOWN - // is handled. - _ignoreWmChar = e.Handled; - } + if(e is RawKeyEventArgs args && args.Key == Key.ImeProcessed) + { + _ignoreWmChar = true; + } + else + { + // Handling a WM_KEYDOWN message should cause the subsequent WM_CHAR message to + // be ignored. This should be safe to do as WM_CHAR should only be produced in + // response to the call to TranslateMessage/DispatchMessage after a WM_KEYDOWN + // is handled. + _ignoreWmChar = e.Handled; + } + } if (s_intermediatePointsPooledList.Count > 0) { From c1ab50b08fd76d54d043e13799452b2dd1dd76df Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 20 Sep 2023 11:17:19 +0200 Subject: [PATCH 13/18] [Mac] Rework raw keyDown/textInput handling (#12774) --- native/Avalonia.Native/src/OSX/AvnView.mm | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index ea6ba93fdb..0508a0557f 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -537,11 +537,13 @@ - (void)keyDown:(NSEvent *)event { _lastKeyHandled = false; - + [self keyboardEvent:event withType:KeyDown]; + + BOOL isKeyDownConsumed = [[self inputContext] handleEvent:event]; if(!_lastKeyHandled){ - [[self inputContext] handleEvent:event]; + _lastKeyHandled = isKeyDownConsumed == YES; } } @@ -552,7 +554,6 @@ } - (void) doCommandBySelector:(SEL)selector{ - } - (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod @@ -599,8 +600,6 @@ - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { - _lastKeyHandled = true; - NSString* markedText; if([string isKindOfClass:[NSAttributedString class]]) @@ -669,8 +668,7 @@ uint64_t timestamp = static_cast([NSDate timeIntervalSinceReferenceDate] * 1000); - _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(timestamp, [text UTF8String]); - + _parent->BaseEvents->RawTextInputEvent(timestamp, [text UTF8String]); } - (NSUInteger)characterIndexForPoint:(NSPoint)point From b15ee692da32dfa349c462c4c491a9cf652325b3 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Wed, 20 Sep 2023 12:28:48 +0300 Subject: [PATCH 14/18] Add support for setting tooltip text for TrayIcons (#12948) --- native/Avalonia.Native/src/OSX/trayicon.h | 2 ++ native/Avalonia.Native/src/OSX/trayicon.mm | 15 +++++++++++++++ src/Avalonia.Native/TrayIconImpl.cs | 2 +- src/Avalonia.Native/avn.idl | 1 + 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/trayicon.h b/native/Avalonia.Native/src/OSX/trayicon.h index f94f9a871b..da30477005 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.h +++ b/native/Avalonia.Native/src/OSX/trayicon.h @@ -28,6 +28,8 @@ public: virtual HRESULT SetMenu (IAvnMenu* menu) override; virtual HRESULT SetIsVisible (bool isVisible) override; + + virtual HRESULT SetToolTipText (char* text) override; }; #endif /* trayicon_h */ diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm index 151990cfb1..5b75b9cc19 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.mm +++ b/native/Avalonia.Native/src/OSX/trayicon.mm @@ -83,3 +83,18 @@ HRESULT AvnTrayIcon::SetIsVisible(bool isVisible) return S_OK; } + +HRESULT AvnTrayIcon::SetToolTipText(char* text) +{ + START_COM_CALL; + + @autoreleasepool + { + if (text != nullptr) + { + [[_native button] setToolTip:[NSString stringWithUTF8String:(const char*)text]]; + } + } + + return S_OK; +} diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index abcc61d950..bac21d5811 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -50,7 +50,7 @@ namespace Avalonia.Native public void SetToolTipText(string? text) { - // NOP + _native.SetToolTipText(text); } public void SetIsVisible(bool visible) diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 7eac1d33a8..96aeb93e66 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -810,6 +810,7 @@ interface IAvnTrayIcon : IUnknown HRESULT SetIcon(void* data, size_t length); HRESULT SetMenu(IAvnMenu* menu); HRESULT SetIsVisible(bool isVisible); + HRESULT SetToolTipText(char* text); } [uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)] From d43dde504e5ab60f95a19850076d32c611c72368 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 21 Sep 2023 01:13:56 +0200 Subject: [PATCH 15/18] FluentTheme: settable ListBoxItem.FontWeight/FontSize (#12958) --- src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml index 4e74406c92..024c379c11 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml @@ -14,9 +14,12 @@ 12,9,12,12 + Normal + + From 3515b99a921851d90b10059a548393e9df0d4b8b Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 21 Sep 2023 01:14:37 +0200 Subject: [PATCH 16/18] Fix VirtualizingStackPanel and nth-child for the currently realizing item container (#12957) * Added failing tests for VirtualizingStackPanel and nth-child * VirtualizingStackPanel: support index of currently realizing item --- .../VirtualizingStackPanel.cs | 10 ++++ .../VirtualizingStackPanelTests.cs | 54 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 6a3c7e17ee..2d9c19b859 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -72,6 +72,8 @@ namespace Avalonia.Controls private Dictionary>? _recyclePool; private Control? _focusedElement; private int _focusedIndex = -1; + private Control? _realizingElement; + private int _realizingIndex = -1; public VirtualizingStackPanel() { @@ -336,6 +338,8 @@ namespace Avalonia.Controls return _scrollToElement; if (_focusedIndex == index) return _focusedElement; + if (index == _realizingIndex) + return _realizingElement; if (GetRealizedElement(index) is { } realized) return realized; if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer) @@ -349,6 +353,8 @@ namespace Avalonia.Controls return _scrollToIndex; if (container == _focusedElement) return _focusedIndex; + if (container == _realizingElement) + return _realizingIndex; return _realizedElements?.GetIndex(container) ?? -1; } @@ -532,7 +538,9 @@ namespace Avalonia.Controls // Start at the anchor element and move forwards, realizing elements. do { + _realizingIndex = index; var e = GetOrCreateElement(items, index); + _realizingElement = e; e.Measure(availableSize); var sizeU = horizontal ? e.DesiredSize.Width : e.DesiredSize.Height; @@ -543,6 +551,8 @@ namespace Avalonia.Controls u += sizeU; ++index; + _realizingIndex = -1; + _realizingElement = null; } while (u < viewport.viewportUEnd && index < items.Count); // Store the last index and end U position for the desired size calculation. diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index ba4fb32067..0aaa28e5ae 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -501,6 +501,33 @@ namespace Avalonia.Controls.UnitTests } } + // https://github.com/AvaloniaUI/Avalonia/issues/12838 + [Fact] + public void NthChild_Selector_Works_For_ItemTemplate_Children() + { + using var app = App(); + + var style = new Style(x => x.OfType().NthChild(5, 0).Child().OfType()) + { + Setters = { new Setter(Panel.BackgroundProperty, Brushes.Red) }, + }; + + var (target, _, _) = CreateTarget(styles: new[] { style }); + var realized = target.GetRealizedContainers()!.Cast().ToList(); + + Assert.Equal(10, realized.Count); + + for (var i = 0; i < 10; ++i) + { + var container = realized[i]; + var index = target.IndexFromContainer(container); + var expectedBackground = (i == 4 || i == 9) ? Brushes.Red : null; + + Assert.Equal(i, index); + Assert.Equal(expectedBackground, ((Canvas) container.Child!).Background); + } + } + [Fact] public void NthLastChild_Selector_Works() { @@ -527,6 +554,33 @@ namespace Avalonia.Controls.UnitTests } } + // https://github.com/AvaloniaUI/Avalonia/issues/12838 + [Fact] + public void NthLastChild_Selector_Works_For_ItemTemplate_Children() + { + using var app = App(); + + var style = new Style(x => x.OfType().NthLastChild(5, 0).Child().OfType()) + { + Setters = { new Setter(Panel.BackgroundProperty, Brushes.Red) }, + }; + + var (target, _, _) = CreateTarget(styles: new[] { style }); + var realized = target.GetRealizedContainers()!.Cast().ToList(); + + Assert.Equal(10, realized.Count); + + for (var i = 0; i < 10; ++i) + { + var container = realized[i]; + var index = target.IndexFromContainer(container); + var expectedBackground = (i == 0 || i == 5) ? Brushes.Red : null; + + Assert.Equal(i, index); + Assert.Equal(expectedBackground, ((Canvas) container.Child!).Background); + } + } + [Fact] public void ContainerPrepared_Is_Raised_When_Scrolling() { From 206b5fd1de645c12efe67c1a916b63d5153a7c12 Mon Sep 17 00:00:00 2001 From: Vladimir Drobyshev <12460363+VladimirDrobyshev@users.noreply.github.com> Date: Thu, 21 Sep 2023 02:14:56 +0300 Subject: [PATCH 17/18] Fix - TextPresenter ignores FontStretch property (#12947) * Test * Fix --- .../Presenters/TextPresenter.cs | 2 +- .../Presenters/TextPresenter_Tests.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 7f99aecd21..df1d37c259 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -476,7 +476,7 @@ namespace Avalonia.Controls.Presenters var caretIndex = CaretIndex; var preeditText = PreeditText; var text = GetCombinedText(Text, caretIndex, preeditText); - var typeface = new Typeface(FontFamily, FontStyle, FontWeight); + var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; var start = Math.Min(selectionStart, selectionEnd); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs index 61ce056c49..69d30883c6 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs @@ -1,5 +1,6 @@ using System.Linq; using Avalonia.Controls.Presenters; +using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -51,5 +52,28 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal("****", actual); } } + + [Theory] + [InlineData(FontStretch.Condensed)] + [InlineData(FontStretch.Expanded)] + [InlineData(FontStretch.Normal)] + [InlineData(FontStretch.ExtraCondensed)] + [InlineData(FontStretch.SemiCondensed)] + [InlineData(FontStretch.ExtraExpanded)] + [InlineData(FontStretch.SemiExpanded)] + [InlineData(FontStretch.UltraCondensed)] + [InlineData(FontStretch.UltraExpanded)] + public void TextPresenter_Should_Use_FontStretch_Property(FontStretch fontStretch) + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var presenter = new TextPresenter { FontStretch = fontStretch, Text = "test" }; + Assert.NotNull(presenter.TextLayout); + Assert.Equal(1, presenter.TextLayout.TextLines.Count); + Assert.Equal(1, presenter.TextLayout.TextLines[0].TextRuns.Count); + Assert.NotNull(presenter.TextLayout.TextLines[0].TextRuns[0].Properties); + Assert.Equal(fontStretch, presenter.TextLayout.TextLines[0].TextRuns[0].Properties.Typeface.Stretch); + } + } } } From cde91774ac9db803041642212eeb2626919c449d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 21 Sep 2023 07:44:10 +0200 Subject: [PATCH 18/18] Restore PathIcon Foreground setter in Fluent theme (#12789) * Restore PathIcon Foreground setter in Fluent theme * Use TextControlForeground resource --- src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml b/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml index 2a817ccfcb..a3395ad99f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml @@ -10,6 +10,7 @@ +