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.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); } 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..af7a58dbe1 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; @@ -26,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)); } /// @@ -95,69 +90,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 (!_fontCollections.TryGetValue(source, out var fontCollection) && (source.IsAbsoluteResm() || source.IsAvares())) + if (TryGetGlyphTypefaceByKeyAndName(typeface, key, familyName, out glyphTypeface) && + glyphTypeface.FamilyName.Contains(familyName)) + { + return true; + } + } + } + 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 +242,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 +260,46 @@ 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; + } + + 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/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/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/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index fbe9370edc..215586eef9 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -1,7 +1,7 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using Avalonia.Platform; using Avalonia.Utilities; @@ -108,19 +108,23 @@ namespace Avalonia.Media.Imaging PlatformImpl = RefCountable.Create(factory.LoadBitmap(format, alphaFormat, data, size, dpi, stride)); else { - var transcoded = Marshal.AllocHGlobal(size.Width * size.Height * 4); - var transcodedStride = size.Width * 4; - try + using (var transcoded = new BitmapMemory(PixelFormat.Rgba8888, Platform.AlphaFormat.Unpremul, size)) { - PixelFormatReader.Transcode(transcoded, data, size, stride, transcodedStride, format); - var transcodedAlphaFormat = format.HasAlpha ? alphaFormat : AlphaFormat.Opaque; - + var transcodedAlphaFormat = format.HasAlpha ? alphaFormat : Platform.AlphaFormat.Opaque; + + PixelFormatTranscoder.Transcode( + data, + size, + stride, + format, + alphaFormat, + transcoded.Address, + transcoded.RowBytes, + transcoded.Format, + transcodedAlphaFormat); + PlatformImpl = RefCountable.Create(factory.LoadBitmap(PixelFormat.Rgba8888, transcodedAlphaFormat, - transcoded, size, dpi, transcodedStride)); - } - finally - { - Marshal.FreeHGlobal(transcoded); + transcoded.Address, size, dpi, transcoded.RowBytes)); } _isTranscoded = true; @@ -173,6 +177,8 @@ namespace Avalonia.Media.Imaging public virtual PixelFormat? Format => (PlatformImpl.Item as IReadableBitmapImpl)?.Format; + public virtual AlphaFormat? AlphaFormat => (PlatformImpl.Item as IReadableBitmapWithAlphaImpl)?.AlphaFormat; + private protected unsafe void CopyPixelsCore(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride, ILockedFramebuffer fb) { @@ -222,6 +228,44 @@ namespace Avalonia.Media.Imaging CopyPixelsCore(sourceRect, buffer, bufferSize, stride, fb); } + /// + /// Copies pixels to the target buffer and transcodes the pixel and alpha format if needed. + /// + /// The target buffer. + /// The alpha format. + /// + public void CopyPixels(ILockedFramebuffer buffer, AlphaFormat alphaFormat) + { + if (PlatformImpl.Item is not IReadableBitmapWithAlphaImpl readable || readable.Format == null || readable.AlphaFormat == null) + { + throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); + } + + if (readable.Format != Format || readable.AlphaFormat != alphaFormat) + { + using (var fb = readable.Lock()) + { + PixelFormatTranscoder.Transcode( + fb.Address, + fb.Size, + fb.RowBytes, + fb.Format, + readable.AlphaFormat.Value, + buffer.Address, + buffer.RowBytes, + buffer.Format, + alphaFormat); + } + } + else + { + using (var fb = readable.Lock()) + { + CopyPixelsCore(new PixelRect(fb.Size), buffer.Address, buffer.RowBytes * buffer.Size.Height, fb.RowBytes, fb); + } + } + } + /// void IImage.Draw( DrawingContext context, diff --git a/src/Avalonia.Base/Media/Imaging/BitmapMemory.cs b/src/Avalonia.Base/Media/Imaging/BitmapMemory.cs index 68ae2e37a5..1032081b6e 100644 --- a/src/Avalonia.Base/Media/Imaging/BitmapMemory.cs +++ b/src/Avalonia.Base/Media/Imaging/BitmapMemory.cs @@ -9,9 +9,10 @@ internal class BitmapMemory : IDisposable { private readonly int _memorySize; - public BitmapMemory(PixelFormat format, PixelSize size) + public BitmapMemory(PixelFormat format, AlphaFormat alphaFormat, PixelSize size) { Format = format; + AlphaFormat = alphaFormat; Size = size; RowBytes = (size.Width * format.BitsPerPixel + 7) / 8; _memorySize = RowBytes * size.Height; @@ -44,8 +45,19 @@ internal class BitmapMemory : IDisposable public int RowBytes { get; } public PixelFormat Format { get; } + public AlphaFormat AlphaFormat { get; } - - public void CopyToRgba(IntPtr buffer, int rowBytes) => - PixelFormatReader.Transcode(buffer, Address, Size, RowBytes, rowBytes, Format); -} \ No newline at end of file + public void CopyToRgba(AlphaFormat alphaFormat, IntPtr buffer, int stride) + { + PixelFormatTranscoder.Transcode( + Address, + Size, + RowBytes, + Format, + AlphaFormat, + buffer, + stride, + PixelFormat.Rgba8888, + alphaFormat); + } +} diff --git a/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs index c90c4cb5ac..df2614d8ff 100644 --- a/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs +++ b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs @@ -2,22 +2,46 @@ using System; using Avalonia.Platform; namespace Avalonia.Media.Imaging; -internal struct Rgba8888Pixel +internal record struct Rgba64Pixel { + public Rgba64Pixel(ushort r, ushort g, ushort b, ushort a) + { + R = r; + G = g; + B = b; + A = a; + } + + public ushort R; + public ushort G; + public ushort B; + public ushort A; +} + +internal record struct Rgba8888Pixel +{ + public Rgba8888Pixel(byte r, byte g, byte b, byte a) + { + R = r; + G = g; + B = b; + A = a; + } + public byte R; public byte G; public byte B; public byte A; } -static unsafe class PixelFormatReader +internal interface IPixelFormatReader +{ + Rgba8888Pixel ReadNext(); + void Reset(IntPtr address); +} + +internal static unsafe class PixelFormatReader { - public interface IPixelFormatReader - { - Rgba8888Pixel ReadNext(); - void Reset(IntPtr address); - } - private static readonly Rgba8888Pixel s_white = new Rgba8888Pixel { A = 255, @@ -25,7 +49,7 @@ static unsafe class PixelFormatReader G = 255, R = 255 }; - + private static readonly Rgba8888Pixel s_black = new Rgba8888Pixel { A = 255, @@ -34,7 +58,7 @@ static unsafe class PixelFormatReader R = 0 }; - public unsafe struct BlackWhitePixelReader : IPixelFormatReader + public unsafe struct BlackWhitePixelFormatReader : IPixelFormatReader { private int _bit; private byte* _address; @@ -58,8 +82,8 @@ static unsafe class PixelFormatReader return value == 1 ? s_white : s_black; } } - - public unsafe struct Gray2PixelReader : IPixelFormatReader + + public unsafe struct Gray2PixelFormatReader : IPixelFormatReader { private int _bit; private byte* _address; @@ -88,7 +112,7 @@ static unsafe class PixelFormatReader { var shift = 6 - _bit; var value = (byte)((*_address >> shift)); - value = (byte)((value & 3)); + value = (byte)((value & 3)); _bit += 2; if (_bit == 8) { @@ -99,8 +123,8 @@ static unsafe class PixelFormatReader return Palette[value]; } } - - public unsafe struct Gray4PixelReader : IPixelFormatReader + + public unsafe struct Gray4PixelFormatReader : IPixelFormatReader { private int _bit; private byte* _address; @@ -133,8 +157,8 @@ static unsafe class PixelFormatReader }; } } - - public unsafe struct Gray8PixelReader : IPixelFormatReader + + public unsafe struct Gray8PixelFormatReader : IPixelFormatReader { private byte* _address; public void Reset(IntPtr address) @@ -156,8 +180,8 @@ static unsafe class PixelFormatReader }; } } - - public unsafe struct Gray16PixelReader : IPixelFormatReader + + public unsafe struct Gray16PixelFormatReader : IPixelFormatReader { private ushort* _address; public Rgba8888Pixel ReadNext() @@ -177,7 +201,7 @@ static unsafe class PixelFormatReader public void Reset(IntPtr address) => _address = (ushort*)address; } - public unsafe struct Gray32FloatPixelReader : IPixelFormatReader + public unsafe struct Gray32FloatPixelFormatReader : IPixelFormatReader { private byte* _address; public Rgba8888Pixel ReadNext() @@ -199,19 +223,10 @@ static unsafe class PixelFormatReader public void Reset(IntPtr address) => _address = (byte*)address; } - struct Rgba64 - { -#pragma warning disable CS0649 - public ushort R; - public ushort G; - public ushort B; - public ushort A; -#pragma warning restore CS0649 - } public unsafe struct Rgba64PixelFormatReader : IPixelFormatReader { - private Rgba64* _address; + private Rgba64Pixel* _address; public Rgba8888Pixel ReadNext() { var value = *_address; @@ -226,9 +241,9 @@ static unsafe class PixelFormatReader }; } - public void Reset(IntPtr address) => _address = (Rgba64*)address; + public void Reset(IntPtr address) => _address = (Rgba64Pixel*)address; } - + public unsafe struct Rgb24PixelFormatReader : IPixelFormatReader { private byte* _address; @@ -247,7 +262,7 @@ static unsafe class PixelFormatReader public void Reset(IntPtr address) => _address = (byte*)address; } - + public unsafe struct Bgr24PixelFormatReader : IPixelFormatReader { private byte* _address; @@ -267,58 +282,105 @@ static unsafe class PixelFormatReader public void Reset(IntPtr address) => _address = (byte*)address; } - public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst, - PixelFormat format) + public unsafe struct Bgr555PixelFormatReader : IPixelFormatReader { - if (format == PixelFormats.BlackWhite) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Gray2) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Gray4) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Gray8) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Gray16) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Rgb24) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Bgr24) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Gray32Float) - Transcode(dst, src, size, strideSrc, strideDst); - else if (format == PixelFormats.Rgba64) - Transcode(dst, src, size, strideSrc, strideDst); - else - throw new NotSupportedException($"Pixel format {format} is not supported"); + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = (ushort*)_address; + + _address += 2; + + return UnPack(*addr); + } + + public void Reset(IntPtr address) => _address = (byte*)address; + + private static Rgba8888Pixel UnPack(ushort value) + { + var r = (byte)Math.Round(((value >> 10) & 0x1F) / 31F * 255); + var g = (byte)Math.Round(((value >> 5) & 0x1F) / 31F * 255); + var b = (byte)Math.Round(((value >> 0) & 0x1F) / 31F * 255); + + return new Rgba8888Pixel(r, g, b, 255); + } } - - public static bool SupportsFormat(PixelFormat format) + + public unsafe struct Bgr565PixelFormatReader : IPixelFormatReader { - return format == PixelFormats.BlackWhite - || format == PixelFormats.Gray2 - || format == PixelFormats.Gray4 - || format == PixelFormats.Gray8 - || format == PixelFormats.Gray16 - || format == PixelFormats.Gray32Float - || format == PixelFormats.Rgba64 - || format == PixelFormats.Bgr24 - || format == PixelFormats.Rgb24; + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = (ushort*)_address; + + _address += 2; + + return UnPack(*addr); + } + + public void Reset(IntPtr address) => _address = (byte*)address; + + private static Rgba8888Pixel UnPack(ushort value) + { + var r = (byte)Math.Round(((value >> 11) & 0x1F) / 31F * 255); + var g = (byte)Math.Round(((value >> 5) & 0x3F) / 63F * 255); + var b = (byte)Math.Round(((value >> 0) & 0x1F) / 31F * 255); + + return new Rgba8888Pixel(r, g, b, 255); + } } - - public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst) where TReader : struct, IPixelFormatReader + + public unsafe struct Rgba8888PixelFormatReader : IPixelFormatReader { - var w = size.Width; - var h = size.Height; - TReader reader = default; - for (var y = 0; y < h; y++) + private Rgba8888Pixel* _address; + public Rgba8888Pixel ReadNext() { - reader.Reset(src + strideSrc * y); - var dstRow = (Rgba8888Pixel*)(dst + strideDst * y); - for (var x = 0; x < w; x++) - { - *dstRow = reader.ReadNext(); - dstRow++; - } + var value = *_address; + + _address++; + + return value; + } + + public void Reset(IntPtr address) => _address = (Rgba8888Pixel*)address; + } + + public unsafe struct Bgra8888PixelFormatReader : IPixelFormatReader + { + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = _address; + + _address += 4; + + return new Rgba8888Pixel(addr[2], addr[1], addr[0], addr[3]); } + + public void Reset(IntPtr address) => _address = (byte*)address; } -} \ No newline at end of file + + public static bool SupportsFormat(PixelFormat format) + { + switch (format.FormatEnum) + { + case PixelFormatEnum.Rgb565: + case PixelFormatEnum.Rgba8888: + case PixelFormatEnum.Bgra8888: + case PixelFormatEnum.BlackWhite: + case PixelFormatEnum.Gray2: + case PixelFormatEnum.Gray4: + case PixelFormatEnum.Gray8: + case PixelFormatEnum.Gray16: + case PixelFormatEnum.Gray32Float: + case PixelFormatEnum.Rgba64: + case PixelFormatEnum.Rgb24: + case PixelFormatEnum.Bgr24: + case PixelFormatEnum.Bgr555: + case PixelFormatEnum.Bgr565: + return true; + default: + return false; + } + } +} diff --git a/src/Avalonia.Base/Media/Imaging/PixelFormatTranscoder.cs b/src/Avalonia.Base/Media/Imaging/PixelFormatTranscoder.cs new file mode 100644 index 0000000000..b91c574fe0 --- /dev/null +++ b/src/Avalonia.Base/Media/Imaging/PixelFormatTranscoder.cs @@ -0,0 +1,154 @@ +using System; +using Avalonia.Platform; +namespace Avalonia.Media.Imaging; + +internal static unsafe class PixelFormatTranscoder +{ + public static void Transcode( + IntPtr source, + PixelSize srcSize, + int sourceStride, + PixelFormat srcFormat, + AlphaFormat srcAlphaFormat, + IntPtr dest, + int destStride, + PixelFormat destFormat, + AlphaFormat destAlphaFormat) + { + var reader = GetReader(srcFormat); + var writer = GetWriter(destFormat); + + var w = srcSize.Width; + var h = srcSize.Height; + + for (var y = 0; y < h; y++) + { + reader.Reset(source + sourceStride * y); + + writer.Reset(dest + destStride * y); + + for (var x = 0; x < w; x++) + { + writer.WriteNext(GetConvertedPixel(reader.ReadNext(), srcAlphaFormat, destAlphaFormat)); + } + } + } + + private static Rgba8888Pixel GetConvertedPixel(Rgba8888Pixel pixel, AlphaFormat sourceAlpha, AlphaFormat destAlpha) + { + if (sourceAlpha != destAlpha) + { + if (sourceAlpha == AlphaFormat.Premul && destAlpha != AlphaFormat.Premul) + { + return ConvertFromPremultiplied(pixel); + } + + if (sourceAlpha != AlphaFormat.Premul && destAlpha == AlphaFormat.Premul) + { + return ConvertToPremultiplied(pixel); + } + } + + return pixel; + } + + private static Rgba8888Pixel ConvertToPremultiplied(Rgba8888Pixel pixel) + { + var factor = pixel.A / 255F; + + return new Rgba8888Pixel + { + R = (byte)(pixel.R * factor), + G = (byte)(pixel.G * factor), + B = (byte)(pixel.B * factor), + A = pixel.A + }; + } + + private static Rgba8888Pixel ConvertFromPremultiplied(Rgba8888Pixel pixel) + { + var factor = 1F / (pixel.A / 255F); + + return new Rgba8888Pixel + { + R = (byte)(pixel.R * factor), + G = (byte)(pixel.G * factor), + B = (byte)(pixel.B * factor), + A = pixel.A + }; + } + + private static IPixelFormatReader GetReader(PixelFormat format) + { + switch (format.FormatEnum) + { + case PixelFormatEnum.Rgb565: + return new PixelFormatReader.Bgr565PixelFormatReader(); + case PixelFormatEnum.Rgba8888: + return new PixelFormatReader.Rgba8888PixelFormatReader(); + case PixelFormatEnum.Bgra8888: + return new PixelFormatReader.Bgra8888PixelFormatReader(); + case PixelFormatEnum.BlackWhite: + return new PixelFormatReader.BlackWhitePixelFormatReader(); + case PixelFormatEnum.Gray2: + return new PixelFormatReader.Gray2PixelFormatReader(); + case PixelFormatEnum.Gray4: + return new PixelFormatReader.Gray4PixelFormatReader(); + case PixelFormatEnum.Gray8: + return new PixelFormatReader.Gray8PixelFormatReader(); + case PixelFormatEnum.Gray16: + return new PixelFormatReader.Gray16PixelFormatReader(); + case PixelFormatEnum.Gray32Float: + return new PixelFormatReader.Gray32FloatPixelFormatReader(); + case PixelFormatEnum.Rgba64: + return new PixelFormatReader.Rgba64PixelFormatReader(); + case PixelFormatEnum.Rgb24: + return new PixelFormatReader.Rgb24PixelFormatReader(); + case PixelFormatEnum.Bgr24: + return new PixelFormatReader.Bgr24PixelFormatReader(); + case PixelFormatEnum.Bgr555: + return new PixelFormatReader.Bgr555PixelFormatReader(); + case PixelFormatEnum.Bgr565: + return new PixelFormatReader.Bgr565PixelFormatReader(); + default: + throw new NotSupportedException($"Pixel format {format} is not supported"); + } + } + + private static IPixelFormatWriter GetWriter(PixelFormat format) + { + switch (format.FormatEnum) + { + case PixelFormatEnum.Rgb565: + return new PixelFormatWriter.Bgr565PixelFormatWriter(); + case PixelFormatEnum.Rgba8888: + return new PixelFormatWriter.Rgba8888PixelFormatWriter(); + case PixelFormatEnum.Bgra8888: + return new PixelFormatWriter.Bgra8888PixelFormatWriter(); + case PixelFormatEnum.BlackWhite: + return new PixelFormatWriter.BlackWhitePixelFormatWriter(); + case PixelFormatEnum.Gray2: + return new PixelFormatWriter.Gray2PixelFormatWriter(); + case PixelFormatEnum.Gray4: + return new PixelFormatWriter.Gray4PixelFormatWriter(); + case PixelFormatEnum.Gray8: + return new PixelFormatWriter.Gray8PixelFormatWriter(); + case PixelFormatEnum.Gray16: + return new PixelFormatWriter.Gray16PixelFormatWriter(); + case PixelFormatEnum.Gray32Float: + return new PixelFormatWriter.Gray32FloatPixelFormatWriter(); + case PixelFormatEnum.Rgba64: + return new PixelFormatWriter.Rgba64PixelFormatWriter(); + case PixelFormatEnum.Rgb24: + return new PixelFormatWriter.Rgb24PixelFormatWriter(); + case PixelFormatEnum.Bgr24: + return new PixelFormatWriter.Bgr24PixelFormatWriter(); + case PixelFormatEnum.Bgr555: + return new PixelFormatWriter.Bgr555PixelFormatWriter(); + case PixelFormatEnum.Bgr565: + return new PixelFormatWriter.Bgr565PixelFormatWriter(); + default: + throw new NotSupportedException($"Pixel format {format} is not supported"); + } + } +} diff --git a/src/Avalonia.Base/Media/Imaging/PixelFormatWriter.cs b/src/Avalonia.Base/Media/Imaging/PixelFormatWriter.cs new file mode 100644 index 0000000000..3c0d5b61f2 --- /dev/null +++ b/src/Avalonia.Base/Media/Imaging/PixelFormatWriter.cs @@ -0,0 +1,324 @@ +using System; +namespace Avalonia.Media.Imaging; + +internal interface IPixelFormatWriter +{ + void WriteNext(Rgba8888Pixel pixel); + void Reset(IntPtr address); +} + +internal static class PixelFormatWriter +{ + public unsafe struct Rgb24PixelFormatWriter : IPixelFormatWriter + { + private byte* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + addr[0] = pixel.R; + addr[1] = pixel.G; + addr[2] = pixel.B; + + _address += 3; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Rgba64PixelFormatWriter : IPixelFormatWriter + { + private Rgba64Pixel* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + *addr = new Rgba64Pixel((ushort)(pixel.R << 8), (ushort)(pixel.G << 8), (ushort)(pixel.B << 8), (ushort)(pixel.A << 8)); + + _address++; + } + + public void Reset(IntPtr address) => _address = (Rgba64Pixel*)address; + } + + public unsafe struct Rgba8888PixelFormatWriter : IPixelFormatWriter + { + private Rgba8888Pixel* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + *addr = pixel; + + _address++; + } + + public void Reset(IntPtr address) => _address = (Rgba8888Pixel*)address; + } + + public unsafe struct Bgra8888PixelFormatWriter : IPixelFormatWriter + { + private byte* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + addr[0] = pixel.B; + addr[1] = pixel.G; + addr[2] = pixel.R; + addr[3] = pixel.A; + + _address += 4; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Bgr24PixelFormatWriter : IPixelFormatWriter + { + private byte* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + addr[2] = pixel.R; + addr[1] = pixel.G; + addr[0] = pixel.B; + + _address += 3; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Bgra32PixelFormatWriter : IPixelFormatWriter + { + private byte* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + addr[3] = pixel.A; + addr[2] = pixel.R; + addr[1] = pixel.G; + addr[0] = pixel.B; + + _address += 4; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Bgr565PixelFormatWriter : IPixelFormatWriter + { + private ushort* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + *addr = Pack(pixel); + + _address++; + } + + public void Reset(IntPtr address) => _address = (ushort*)address; + + private static ushort Pack(Rgba8888Pixel pixel) + { + return (ushort)((((int)Math.Round(pixel.R / 255F * 31F) & 0x1F) << 11) + | (((int)Math.Round(pixel.G / 255F * 63F) & 0x3F) << 5) + | ((int)Math.Round(pixel.B / 255F * 31F) & 0x1F)); + } + } + + public unsafe struct Bgr555PixelFormatWriter : IPixelFormatWriter + { + private ushort* _address; + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + *addr = Pack(pixel); + + _address++; + } + + public void Reset(IntPtr address) => _address = (ushort*)address; + + private static ushort Pack(Rgba8888Pixel pixel) + { + return (ushort)( + (((int)Math.Round(pixel.R / 255F * 31F) & 0x1F) << 10) + | (((int)Math.Round(pixel.G / 255F * 31F) & 0x1F) << 5) + | (((int)Math.Round(pixel.B / 255F * 31F) & 0x1F) << 0)); + } + } + + public unsafe struct Gray32FloatPixelFormatWriter : IPixelFormatWriter + { + private float* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + *addr = Pack(pixel); + + _address++; + } + + private static float Pack(Rgba8888Pixel pixel) + { + return (float)Math.Pow(pixel.R / 255F, 2.2); + } + + public void Reset(IntPtr address) => _address = (float*)address; + } + + public unsafe struct BlackWhitePixelFormatWriter : IPixelFormatWriter + { + private int _bit; + private byte* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + var grayscale = Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + var value = grayscale > 0x7F ? 1 : 0; + + var shift = 7 - _bit; + var mask = 1 << shift; + + *addr = (byte)((*addr & ~mask) | value << shift); + + _bit++; + + if (_bit == 8) + { + _address++; + + _bit = 0; + } + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Gray2PixelFormatWriter : IPixelFormatWriter + { + private int _bit; + private byte* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + var value = 0; + + var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + if (grayscale > 0 && grayscale <= 0x55) + { + //01 + value = 1; + } + + if (grayscale > 0x55 && grayscale <= 0xAA) + { + //10 + + value = 2; + } + + if (grayscale > 0xAA) + { + //11 + value = 3; + } + + var shift = 6 - _bit; + var mask = 3 << shift; + + *addr = (byte)((*addr & ~mask) | value << shift); + + _bit += 2; + + if (_bit == 8) + { + _address++; + _bit = 0; + } + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Gray4PixelFormatWriter : IPixelFormatWriter + { + private int _bit; + private byte* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + var value = (byte)(grayscale / 255F * 0xF); + + var shift = 4 - _bit; + var mask = 0xF << shift; + + *addr = (byte)((*addr & ~mask) | value << shift); + + _bit += 4; + + if (_bit == 8) + { + _address++; + _bit = 0; + } + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Gray8PixelFormatWriter : IPixelFormatWriter + { + private byte* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + *addr = grayscale; + + _address++; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Gray16PixelFormatWriter : IPixelFormatWriter + { + private ushort* _address; + + public void WriteNext(Rgba8888Pixel pixel) + { + var addr = _address; + + var grayscale = (ushort)Math.Round((0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B) * 0x0101); + + *addr = grayscale; + + _address++; + } + + public void Reset(IntPtr address) => _address = (ushort*)address; + } +} + + diff --git a/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs b/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs index 868f4439c4..5ec00f9c7e 100644 --- a/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs @@ -74,7 +74,7 @@ namespace Avalonia.Media.Imaging Dpi, _pixelFormatMemory.Format, () => { using var inner = ((IWriteableBitmapImpl)PlatformImpl.Item).Lock(); - _pixelFormatMemory.CopyToRgba(inner.Address, inner.RowBytes); + _pixelFormatMemory.CopyToRgba(Platform.AlphaFormat.Unpremul, inner.Address, inner.RowBytes); }); } @@ -137,9 +137,10 @@ namespace Avalonia.Media.Imaging if (!PixelFormatReader.SupportsFormat(finalFormat)) throw new NotSupportedException($"Pixel format {finalFormat} is not supported"); - var impl = ri.CreateWriteableBitmap(size, dpi, PixelFormat.Rgba8888, - finalFormat.HasAlpha ? finalAlphaFormat : AlphaFormat.Opaque); - return (impl, new BitmapMemory(finalFormat, size)); + finalAlphaFormat = finalFormat.HasAlpha ? finalAlphaFormat : Platform.AlphaFormat.Opaque; + + var impl = ri.CreateWriteableBitmap(size, dpi, PixelFormat.Rgba8888, finalAlphaFormat); + return (impl, new BitmapMemory(finalFormat, finalAlphaFormat, size)); } private static IPlatformRenderInterface GetFactory() diff --git a/src/Avalonia.Base/Platform/ILockedFramebuffer.cs b/src/Avalonia.Base/Platform/ILockedFramebuffer.cs index 2f631142d2..f963b77cd9 100644 --- a/src/Avalonia.Base/Platform/ILockedFramebuffer.cs +++ b/src/Avalonia.Base/Platform/ILockedFramebuffer.cs @@ -28,5 +28,7 @@ namespace Avalonia.Platform /// Pixel format /// PixelFormat Format { get; } + + //TODO12: Add AlphaFormat } } diff --git a/src/Avalonia.Base/Platform/IReadableBitmapImpl.cs b/src/Avalonia.Base/Platform/IReadableBitmapImpl.cs index acf1801e0a..d5a0c765cc 100644 --- a/src/Avalonia.Base/Platform/IReadableBitmapImpl.cs +++ b/src/Avalonia.Base/Platform/IReadableBitmapImpl.cs @@ -1,7 +1,16 @@ +using Avalonia.Metadata; + namespace Avalonia.Platform; public interface IReadableBitmapImpl { PixelFormat? Format { get; } ILockedFramebuffer Lock(); -} \ No newline at end of file +} + +//TODO12: Remove me once we can change IReadableBitmapImpl +[Unstable] +public interface IReadableBitmapWithAlphaImpl : IReadableBitmapImpl +{ + AlphaFormat? AlphaFormat { get; } +} diff --git a/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs b/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs index 3284d34a0a..685491a326 100644 --- a/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs +++ b/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs @@ -6,7 +6,7 @@ namespace Avalonia.Platform /// Defines the platform-specific interface for a . /// [Unstable] - public interface IWriteableBitmapImpl : IBitmapImpl, IReadableBitmapImpl + public interface IWriteableBitmapImpl : IBitmapImpl, IReadableBitmapWithAlphaImpl { } } diff --git a/src/Avalonia.Base/Platform/PixelFormat.cs b/src/Avalonia.Base/Platform/PixelFormat.cs index 95f49bdb25..d03b4e04b8 100644 --- a/src/Avalonia.Base/Platform/PixelFormat.cs +++ b/src/Avalonia.Base/Platform/PixelFormat.cs @@ -15,7 +15,9 @@ namespace Avalonia.Platform Gray32Float, Rgba64, Rgb24, - Bgr24 + Bgr24, + Bgr555, + Bgr565 } public record struct PixelFormat @@ -34,8 +36,10 @@ namespace Avalonia.Platform return 4; else if (FormatEnum == PixelFormatEnum.Gray8) return 8; - else if (FormatEnum == PixelFormatEnum.Rgb565 - || FormatEnum == PixelFormatEnum.Gray16) + else if (FormatEnum == PixelFormatEnum.Rgb565 || + FormatEnum == PixelFormatEnum.Bgr555 || + FormatEnum == PixelFormatEnum.Bgr565 || + FormatEnum == PixelFormatEnum.Gray16) return 16; else if (FormatEnum is PixelFormatEnum.Bgr24 or PixelFormatEnum.Rgb24) return 24; @@ -76,5 +80,7 @@ namespace Avalonia.Platform public static PixelFormat Gray32Float { get; } = new PixelFormat(PixelFormatEnum.Gray32Float); public static PixelFormat Rgb24 { get; } = new PixelFormat(PixelFormatEnum.Rgb24); public static PixelFormat Bgr24 { get; } = new PixelFormat(PixelFormatEnum.Bgr24); + public static PixelFormat Bgr555 { get; } = new PixelFormat(PixelFormatEnum.Bgr555); + public static PixelFormat Bgr565 { get; } = new PixelFormat(PixelFormatEnum.Bgr565); } } 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.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/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) 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 diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 63940294a9..5d603143de 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -406,7 +406,10 @@ namespace Avalonia.Headless public Vector Dpi { get; } public PixelSize PixelSize { get; } + public PixelFormat? Format { get; } + public AlphaFormat? AlphaFormat { get; } public int Version { get; set; } + public void Save(string fileName, int? quality = null) { @@ -417,7 +420,6 @@ namespace Avalonia.Headless } - public PixelFormat? Format { get; } public ILockedFramebuffer Lock() { 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/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; + } + } } } diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 0627407509..6051985cea 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -10,7 +10,7 @@ namespace Avalonia.Skia /// /// Immutable Skia bitmap. /// - internal class ImmutableBitmap : IDrawableBitmapImpl, IReadableBitmapImpl + internal class ImmutableBitmap : IDrawableBitmapImpl, IReadableBitmapWithAlphaImpl { private readonly SKImage _image; private readonly SKBitmap? _bitmap; @@ -177,6 +177,9 @@ namespace Avalonia.Skia } public PixelFormat? Format => _bitmap?.ColorType.ToAvalonia(); + + public AlphaFormat? AlphaFormat => _bitmap?.AlphaType.ToAlphaFormat(); + public ILockedFramebuffer Lock() { if (_bitmap is null) diff --git a/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs b/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs index 454e137389..a6a8fd1def 100644 --- a/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs @@ -14,7 +14,7 @@ internal class RenderTargetBitmapImpl : WriteableBitmapImpl, public RenderTargetBitmapImpl(PixelSize size, Vector dpi) : base(size, dpi, SKImageInfo.PlatformColorType == SKColorType.Rgba8888 ? PixelFormats.Rgba8888 : PixelFormat.Bgra8888, - AlphaFormat.Premul) + Platform.AlphaFormat.Premul) { _renderTarget = new FramebufferRenderTarget(this); } diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index 581470fcfe..865bcae99c 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -148,6 +148,8 @@ namespace Avalonia.Skia public PixelFormat? Format => _bitmap.ColorType.ToAvalonia(); + public AlphaFormat? AlphaFormat => _bitmap.AlphaType.ToAlphaFormat(); + /// public ILockedFramebuffer Lock() => new BitmapFramebuffer(this, _bitmap); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index 2fa1e5bd7a..5a8f30a235 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -13,7 +13,7 @@ namespace Avalonia.Direct2D1.Media /// /// A WIC implementation of a . /// - internal class WicBitmapImpl : BitmapImpl, IReadableBitmapImpl + internal class WicBitmapImpl : BitmapImpl, IReadableBitmapWithAlphaImpl { private readonly BitmapDecoder _decoder; @@ -87,10 +87,11 @@ namespace Avalonia.Direct2D1.Media if (!alphaFormat.HasValue) { - alphaFormat = AlphaFormat.Premul; + alphaFormat = Platform.AlphaFormat.Premul; } PixelFormat = pixelFormat; + AlphaFormat = alphaFormat; WicImpl = new Bitmap( Direct2D1Platform.ImagingFactory, size.Width, @@ -106,6 +107,7 @@ namespace Avalonia.Direct2D1.Media WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, size.Width, size.Height, format.ToWic(alphaFormat), BitmapCreateCacheOption.CacheOnDemand); WicImpl.SetResolution(dpi.X, dpi.Y); PixelFormat = format; + AlphaFormat = alphaFormat; Dpi = dpi; using (var l = WicImpl.Lock(BitmapLockFlags.Write)) @@ -161,7 +163,9 @@ namespace Avalonia.Direct2D1.Media public override PixelSize PixelSize => WicImpl.Size.ToAvalonia(); - protected APixelFormat? PixelFormat { get; } + public APixelFormat? PixelFormat { get; } + + public AlphaFormat? AlphaFormat { get; } public override void Dispose() { 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(); 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, diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index 4cdce8df26..3cb85a5645 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -3,6 +3,7 @@ net6.0 Library true + 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.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); + } + } } } diff --git a/tests/Avalonia.Base.UnitTests/Media/Imaging/PixelFormatWriterTests.cs b/tests/Avalonia.Base.UnitTests/Media/Imaging/PixelFormatWriterTests.cs new file mode 100644 index 0000000000..daf057ff33 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/Imaging/PixelFormatWriterTests.cs @@ -0,0 +1,355 @@ +using System; +using Avalonia.Media.Imaging; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media.Imaging +{ + public class PixelFormatWriterTests + { + private static readonly Rgba8888Pixel s_white = new Rgba8888Pixel + { + A = 255, + B = 255, + G = 255, + R = 255 + }; + + private static readonly Rgba8888Pixel s_black = new Rgba8888Pixel + { + A = 255, + B = 0, + G = 0, + R = 0 + }; + + [Fact] + public void Should_Write_Bgr555() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Bgr555), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Bgr555PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Bgr555PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(new Rgba8888Pixel { R = 255, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { G = 255 }); + Assert.Equal(new Rgba8888Pixel { G = 255, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { B = 255 }); + Assert.Equal(new Rgba8888Pixel { B = 255, A = 255 }, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Bgra8888() + { + var sourceMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Bgra8888), + Platform.AlphaFormat.Unpremul, + new PixelSize(3, 1)); + + var sourceWriter = new PixelFormatWriter.Bgra8888PixelFormatWriter(); + var sourceReader = new PixelFormatReader.Bgra8888PixelFormatReader(); + + sourceWriter.Reset(sourceMemory.Address); + sourceReader.Reset(sourceMemory.Address); + + sourceWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(new Rgba8888Pixel { R = 255 }, sourceReader.ReadNext()); + + sourceWriter.WriteNext(new Rgba8888Pixel { G = 255 }); + Assert.Equal(new Rgba8888Pixel { G = 255 }, sourceReader.ReadNext()); + + sourceWriter.WriteNext(new Rgba8888Pixel { B = 255 }); + Assert.Equal(new Rgba8888Pixel { B = 255 }, sourceReader.ReadNext()); + } + + [Fact] + public void Should_Write_Rgba8888() + { + var sourceMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Rgba8888), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Rgba8888PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Rgba8888PixelFormatReader(); + + pixelWriter.Reset(sourceMemory.Address); + pixelReader.Reset(sourceMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Rgb24() + { + var sourceMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Rgb24), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Rgb24PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Rgb24PixelFormatReader(); + + pixelWriter.Reset(sourceMemory.Address); + pixelReader.Reset(sourceMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255, G = 125, B = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 255, B = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 125, B = 255 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 255 }, pixelReader.ReadNext()); + } + + + [Fact] + public void Should_Write_Rgba64() + { + var sourceMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Rgba64), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Rgba64PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Rgba64PixelFormatReader(); + + pixelWriter.Reset(sourceMemory.Address); + pixelReader.Reset(sourceMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 255, G = 125, B = 125, A = 125 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 255, B = 125, A = 125 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 255, A = 125 }, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Bgr565() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Bgr565), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Bgr565PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Bgr565PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(new Rgba8888Pixel { R = 255, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { G = 255 }); + Assert.Equal(new Rgba8888Pixel { G = 255, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { B = 255 }); + Assert.Equal(new Rgba8888Pixel { B = 255, A = 255 }, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Gray32Float() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Gray32Float), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Gray32FloatPixelFormatWriter(); + var pixelReader = new PixelFormatReader.Gray32FloatPixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(new Rgba8888Pixel { R = 255, G = 255, B = 255, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 125 }); + Assert.Equal(new Rgba8888Pixel { R = 125, G = 125, B = 125, A = 255 }, pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel()); + Assert.Equal(new Rgba8888Pixel { A = 255 }, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_BlackWhite() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.BlackWhite), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.BlackWhitePixelFormatWriter(); + var pixelReader = new PixelFormatReader.BlackWhitePixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(s_white); + Assert.Equal(s_white, pixelReader.ReadNext()); + + pixelWriter.WriteNext(s_black); + Assert.Equal(s_black, pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Gray2() + { + var palette = new[] + { + s_black, + new Rgba8888Pixel + { + A = 255, B = 0x55, G = 0x55, R = 0x55 + }, + new Rgba8888Pixel + { + A = 255, B = 0xAA, G = 0xAA, R = 0xAA + }, + s_white + }; + + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Gray2), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Gray2PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Gray2PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(palette[0]); + Assert.Equal(palette[0], pixelReader.ReadNext()); + + pixelWriter.WriteNext(palette[1]); + Assert.Equal(palette[1], pixelReader.ReadNext()); + + pixelWriter.WriteNext(palette[2]); + Assert.Equal(palette[2], pixelReader.ReadNext()); + + pixelWriter.WriteNext(palette[3]); + Assert.Equal(palette[3], pixelReader.ReadNext()); + } + + [Fact] + public void Should_Write_Gray4() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Gray4), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Gray4PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Gray4PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(GetGray4(new Rgba8888Pixel { R = 255 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 17 }); + Assert.Equal(GetGray4(new Rgba8888Pixel { R = 17 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel()); + Assert.Equal(new Rgba8888Pixel { A = 255 }, pixelReader.ReadNext()); + } + + private static Rgba8888Pixel GetGray4(Rgba8888Pixel pixel) + { + var grayscale = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + var value = (byte)(grayscale / 255F * 0xF); + + value = (byte)(value | (value << 4)); + + return new Rgba8888Pixel(value, value, value, 255); + } + + [Fact] + public void Should_Write_Gray8() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Gray8), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Gray8PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Gray8PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(GetGray8(new Rgba8888Pixel { R = 255 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 120 }); + Assert.Equal(GetGray8(new Rgba8888Pixel { R = 120 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel()); + Assert.Equal(GetGray8(new Rgba8888Pixel { A = 255 }), pixelReader.ReadNext()); + } + + private static Rgba8888Pixel GetGray8(Rgba8888Pixel pixel) + { + var value = (byte)Math.Round(0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B); + + return new Rgba8888Pixel(value, value, value, 255); + } + + [Fact] + public void Should_Write_Gray16() + { + var bitmapMemory = new BitmapMemory( + new Platform.PixelFormat(Platform.PixelFormatEnum.Gray16), + Platform.AlphaFormat.Unpremul, + new PixelSize(10, 10)); + + var pixelWriter = new PixelFormatWriter.Gray16PixelFormatWriter(); + var pixelReader = new PixelFormatReader.Gray16PixelFormatReader(); + + pixelWriter.Reset(bitmapMemory.Address); + pixelReader.Reset(bitmapMemory.Address); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 255 }); + Assert.Equal(GetGray16(new Rgba8888Pixel { R = 255 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel { R = 120 }); + Assert.Equal(GetGray16(new Rgba8888Pixel { R = 120 }), pixelReader.ReadNext()); + + pixelWriter.WriteNext(new Rgba8888Pixel()); + Assert.Equal(GetGray16(new Rgba8888Pixel { A = 255 }), pixelReader.ReadNext()); + } + + private static Rgba8888Pixel GetGray16(Rgba8888Pixel pixel) + { + var grayscale = (ushort)Math.Round((0.299F * pixel.R + 0.587F * pixel.G + 0.114F * pixel.B) * 0x0101); + + var value = (byte)(grayscale >> 8); + + return new Rgba8888Pixel(value, value, value, 255); + } + } +} 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/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); + } + } + } } } 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(); +}