Browse Source

Merge branch 'master' into fixes/browserIme

pull/12970/head^2
Benedikt Stebner 3 years ago
committed by GitHub
parent
commit
8147a919cf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      Avalonia.sln
  2. 2
      build/SourceGenerators.props
  3. 12
      native/Avalonia.Native/src/OSX/AvnView.mm
  4. 2
      native/Avalonia.Native/src/OSX/trayicon.h
  5. 15
      native/Avalonia.Native/src/OSX/trayicon.mm
  6. 10
      src/Avalonia.Base/Animation/Animatable.cs
  7. 16
      src/Avalonia.Base/Media/CompositeFontFamilyKey.cs
  8. 104
      src/Avalonia.Base/Media/FontFamily.cs
  9. 158
      src/Avalonia.Base/Media/FontManager.cs
  10. 17
      src/Avalonia.Base/Media/FontSourceIdentifier.cs
  11. 14
      src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs
  12. 6
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  13. 16
      src/Avalonia.Base/Media/IGlyphTypeface2.cs
  14. 2
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  15. 70
      src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs
  16. 2
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  17. 2
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  18. 10
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  19. 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  20. 2
      src/Avalonia.Native/TrayIconImpl.cs
  21. 1
      src/Avalonia.Native/avn.idl
  22. 7
      src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml
  23. 1
      src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml
  24. 172
      src/Avalonia.X11/Screens/X11Screen.Providers.cs
  25. 249
      src/Avalonia.X11/Screens/X11Screens.Scaling.cs
  26. 93
      src/Avalonia.X11/Screens/X11Screens.cs
  27. 12
      src/Avalonia.X11/TransparencyHelper.cs
  28. 29
      src/Avalonia.X11/X11Globals.cs
  29. 8
      src/Avalonia.X11/X11Platform.cs
  30. 2
      src/Avalonia.X11/X11PlatformThreading.cs
  31. 337
      src/Avalonia.X11/X11Screens.cs
  32. 13
      src/Avalonia.X11/X11Window.cs
  33. 75
      src/Avalonia.X11/XResources.cs
  34. 59
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  35. 26
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  36. 19
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  37. 8
      src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs
  38. 72
      src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs
  39. 41
      src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs
  40. 2
      tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs
  41. 19
      tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
  42. 39
      tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj
  43. 38
      tests/Avalonia.Build.Tasks.UnitTest/CompileAvaloniaXamlTaskTest.cs
  44. 96
      tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngine.cs
  45. 39
      tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngineMessage.cs
  46. 24
      tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs
  47. 54
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
  48. 12
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  49. 78
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
  50. 8
      tests/TestFiles/BuildTasks/PInvoke/App.axaml
  51. 21
      tests/TestFiles/BuildTasks/PInvoke/App.axaml.cs
  52. 4
      tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml
  53. 22
      tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml.cs
  54. 26
      tests/TestFiles/BuildTasks/PInvoke/PInvoke.csproj
  55. 14
      tests/TestFiles/BuildTasks/PInvoke/Program.cs

22
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}

2
build/SourceGenerators.props

@ -15,7 +15,7 @@
<ItemGroup Condition="'$(IncludeAvaloniaGenerators)' == 'true'">
<ProjectReference
Include="../../src/tools/Avalonia.Generators/Avalonia.Generators.csproj"
Include="$(MSBuildThisFileDirectory)/../src/tools/Avalonia.Generators/Avalonia.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
PrivateAssets="all" />

12
native/Avalonia.Native/src/OSX/AvnView.mm

@ -537,11 +537,13 @@
- (void)keyDown:(NSEvent *)event
{
_lastKeyHandled = false;
[self keyboardEvent:event withType:KeyDown];
BOOL isKeyDownConsumed = [[self inputContext] handleEvent:event];
if(!_lastKeyHandled){
[[self inputContext] handleEvent:event];
_lastKeyHandled = isKeyDownConsumed == YES;
}
}
@ -552,7 +554,6 @@
}
- (void) doCommandBySelector:(SEL)selector{
}
- (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod
@ -599,8 +600,6 @@
- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange
{
_lastKeyHandled = true;
NSString* markedText;
if([string isKindOfClass:[NSAttributedString class]])
@ -669,8 +668,7 @@
uint64_t timestamp = static_cast<uint64_t>([NSDate timeIntervalSinceReferenceDate] * 1000);
_lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(timestamp, [text UTF8String]);
_parent->BaseEvents->RawTextInputEvent(timestamp, [text UTF8String]);
}
- (NSUInteger)characterIndexForPoint:(NSPoint)point

2
native/Avalonia.Native/src/OSX/trayicon.h

@ -28,6 +28,8 @@ public:
virtual HRESULT SetMenu (IAvnMenu* menu) override;
virtual HRESULT SetIsVisible (bool isVisible) override;
virtual HRESULT SetToolTipText (char* text) override;
};
#endif /* trayicon_h */

15
native/Avalonia.Native/src/OSX/trayicon.mm

@ -83,3 +83,18 @@ HRESULT AvnTrayIcon::SetIsVisible(bool isVisible)
return S_OK;
}
HRESULT AvnTrayIcon::SetToolTipText(char* text)
{
START_COM_CALL;
@autoreleasepool
{
if (text != nullptr)
{
[[_native button] setToolTip:[NSString stringWithUTF8String:(const char*)text]];
}
}
return S_OK;
}

10
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);
}

16
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<FontFamilyKey> Keys { get; }
}
}

104
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);
}
}
/// <summary>
@ -88,44 +113,49 @@ namespace Avalonia.Media
return new FontFamily(s);
}
private struct FontFamilyIdentifier
private static FrugalStructList<FontSourceIdentifier> 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<FontSourceIdentifier>(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;
}
/// <summary>

158
src/Avalonia.Base/Media/FontManager.cs

@ -15,9 +15,11 @@ namespace Avalonia.Media
/// </summary>
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<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
@ -26,20 +28,13 @@ namespace Avalonia.Media
{
PlatformImpl = platformImpl;
var options = AvaloniaLocator.Current.GetService<FontManagerOptions>();
AddFontCollection(new SystemFontCollection(this));
var options = AvaloniaLocator.Current.GetService<FontManagerOptions>();
_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));
}
/// <summary>
@ -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;
}
/// <summary>
@ -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;
}
}
}

17
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; }
}
}

14
src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs

@ -28,6 +28,20 @@ namespace Avalonia.Media.Fonts
HasFallbacks = _names.Length > 1;
}
internal FamilyNameCollection(FrugalStructList<FontSourceIdentifier> 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);

6
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;
}

16
src/Avalonia.Base/Media/IGlyphTypeface2.cs

@ -0,0 +1,16 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace Avalonia.Media
{
internal interface IGlyphTypeface2 : IGlyphTypeface
{
/// <summary>
/// Returns the font file stream represented by the <see cref="IGlyphTypeface"/> object.
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns>Returns <c>true</c> if the stream can be obtained, otherwise <c>false</c>.</returns>
bool TryGetStream([NotNullWhen(true)] out Stream? stream);
}
}

2
src/Avalonia.Base/Media/Imaging/Bitmap.cs

@ -241,7 +241,7 @@ namespace Avalonia.Media.Imaging
throw new NotSupportedException("CopyPixels is not supported for this bitmap type");
}
if (readable.Format != Format || readable.AlphaFormat != alphaFormat)
if (buffer.Format != readable.Format || alphaFormat != readable.AlphaFormat)
{
using (var fb = readable.Lock())
{

70
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<TimeSpan>? _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<TimeSpan> 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;
}

2
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++)

2
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -476,7 +476,7 @@ namespace Avalonia.Controls.Presenters
var caretIndex = CaretIndex;
var preeditText = PreeditText;
var text = GetCombinedText(Text, caretIndex, preeditText);
var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);

10
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -72,6 +72,8 @@ namespace Avalonia.Controls
private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _focusedElement;
private int _focusedIndex = -1;
private Control? _realizingElement;
private int _realizingIndex = -1;
public VirtualizingStackPanel()
{
@ -336,6 +338,8 @@ namespace Avalonia.Controls
return _scrollToElement;
if (_focusedIndex == index)
return _focusedElement;
if (index == _realizingIndex)
return _realizingElement;
if (GetRealizedElement(index) is { } realized)
return realized;
if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer)
@ -349,6 +353,8 @@ namespace Avalonia.Controls
return _scrollToIndex;
if (container == _focusedElement)
return _focusedIndex;
if (container == _realizingElement)
return _realizingIndex;
return _realizedElements?.GetIndex(container) ?? -1;
}
@ -532,7 +538,9 @@ namespace Avalonia.Controls
// Start at the anchor element and move forwards, realizing elements.
do
{
_realizingIndex = index;
var e = GetOrCreateElement(items, index);
_realizingElement = e;
e.Measure(availableSize);
var sizeU = horizontal ? e.DesiredSize.Width : e.DesiredSize.Height;
@ -543,6 +551,8 @@ namespace Avalonia.Controls
u += sizeU;
++index;
_realizingIndex = -1;
_realizingElement = null;
} while (u < viewport.viewportUEnd && index < items.Count);
// Store the last index and end U position for the desired size calculation.

2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -108,7 +108,7 @@ namespace Avalonia.Native
.Bind<IPlatformSettings>().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings()))
.Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))
.Bind<IRenderTimer>().ToConstant(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer()))
.Bind<IRenderTimer>().ToConstant(new ThreadProxyRenderTimer(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer())))
.Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory))
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(applicationPlatform)

2
src/Avalonia.Native/TrayIconImpl.cs

@ -50,7 +50,7 @@ namespace Avalonia.Native
public void SetToolTipText(string? text)
{
// NOP
_native.SetToolTipText(text);
}
public void SetIsVisible(bool visible)

1
src/Avalonia.Native/avn.idl

@ -810,6 +810,7 @@ interface IAvnTrayIcon : IUnknown
HRESULT SetIcon(void* data, size_t length);
HRESULT SetMenu(IAvnMenu* menu);
HRESULT SetIsVisible(bool isVisible);
HRESULT SetToolTipText(char* text);
}
[uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)]

7
src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml

@ -14,9 +14,12 @@
</Border>
</Design.PreviewWith>
<Thickness x:Key="ListBoxItemPadding">12,9,12,12</Thickness>
<FontWeight x:Key="ListBoxItemFontWeight">Normal</FontWeight>
<ControlTheme x:Key="{x:Type ListBoxItem}" TargetType="ListBoxItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Padding" Value="{DynamicResource ListBoxItemPadding}" />
<Setter Property="FontWeight" Value="{DynamicResource ListBoxItemFontWeight}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"
@ -26,8 +29,8 @@
CornerRadius="{TemplateBinding CornerRadius}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
FontWeight="Normal"
FontSize="{DynamicResource ControlContentThemeFontSize}"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" />

1
src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml

@ -10,6 +10,7 @@
</StackPanel>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type PathIcon}" TargetType="PathIcon">
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="{DynamicResource IconElementThemeHeight}" />
<Setter Property="Width" Value="{DynamicResource IconElementThemeWidth}" />

172
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) };
}
}

249
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<string, double>? _namedConfig;
private readonly List<double>? _indexedConfig;
public UserConfiguredScalingProvider(Dictionary<string, double>? namedConfig, List<double>? 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<string, double>? NamedConfig { get; set; }
public List<double>? 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;
}
}

93
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<Screen> AllScreens =>
_impl.Screens.Select((s, i) => new Screen(_scaling.GetScaling(s, i), s.Bounds, s.WorkingArea, s.IsPrimary))
.ToArray();
}
}

12
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<WindowTransparencyLevel> 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();
}
}

29
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<IGlobalsSubscriber> _subscribers = new List<IGlobalsSubscriber>();
private string _wmName;
private IntPtr _compositionAtomOwner;
private bool _isCompositionEnabled;
public event Action WindowManagerChanged;
public event Action CompositionChanged;
public event Action<IntPtr> 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);
}
}

8
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<IntPtr, X11PlatformThreading.EventHandler>();
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<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind<IPlatformLifetimeEventsImpl>().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();

2
src/Avalonia.X11/X11PlatformThreading.cs

@ -235,7 +235,7 @@ namespace Avalonia.X11
}
public long Now => (int)_clock.ElapsedMilliseconds;
public long Now => _clock.ElapsedMilliseconds;
public bool CanQueryPendingInput => true;
public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || XPending(_display) != 0;

337
src/Avalonia.X11/X11Screens.cs

@ -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<Screen> 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<string, double> 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();
}
}
}
}

13
src/Avalonia.X11/X11Window.cs

@ -230,6 +230,8 @@ namespace Avalonia.X11
() => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult<IStorageProvider?>(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();

75
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<string, string> _resources = new();
private readonly X11Info _x11;
public event Action<string>? 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<string, string>();
var missingResources = new HashSet<string>(_resources.Keys);
var changedResources = new HashSet<string>();
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();
}
}

59
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

26
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;
}
}
}
}

19
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -775,12 +775,19 @@ namespace Avalonia.Win32
if (message == WindowsMessage.WM_KEYDOWN)
{
// Handling a WM_KEYDOWN message should cause the subsequent WM_CHAR message to
// be ignored. This should be safe to do as WM_CHAR should only be produced in
// response to the call to TranslateMessage/DispatchMessage after a WM_KEYDOWN
// is handled.
_ignoreWmChar = e.Handled;
}
if(e is RawKeyEventArgs args && args.Key == Key.ImeProcessed)
{
_ignoreWmChar = true;
}
else
{
// Handling a WM_KEYDOWN message should cause the subsequent WM_CHAR message to
// be ignored. This should be safe to do as WM_CHAR should only be produced in
// response to the call to TranslateMessage/DispatchMessage after a WM_KEYDOWN
// is handled.
_ignoreWmChar = e.Handled;
}
}
if (s_intermediatePointsPooledList.Count > 0)
{

8
src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs

@ -29,15 +29,13 @@ namespace Avalonia.iOS
public bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
var builder = AppBuilder.Configure<TApp>().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();

72
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<ISymbol>(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<ISymbol, AvaloniaPropertyDescription> propertyDescriptions)
private void RegisterInitializer_Invocation(IInvocationOperation invocation, ISymbol target, ConcurrentDictionary<ISymbol, AvaloniaPropertyDescription> 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;
}
}
}

41
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
/// <summary>
/// Follows assignments and conversions back to their source.
/// </summary>
private static IOperation ResolveOperationSource(IOperation operation)
private static IOperation ResolveOperationSource(IOperation operation, CancellationToken cancellationToken)
{
var seen = new HashSet<IOperation>();
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<IOperation>();
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;
/// <exception cref="AvaloniaAnalysisException"/>
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,

2
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());
}

19
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<InvalidOperationException>(() => 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);
}
}
}
}

39
tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj

@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<OutputType>Library</OutputType>
<IsPackable>false</IsPackable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configuration Condition="'$(Configuration)'==''">Debug</Configuration>
</PropertyGroup>
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<Content Include="..\TestFiles\BuildTasks\PInvoke\bin\$(Configuration)\netstandard2.0\PInvoke.dll" Link="Assets\PInvoke.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\TestFiles\BuildTasks\PInvoke\bin\$(Configuration)\netstandard2.0\PInvoke.dll.refs" Link="Assets\PInvoke.dll.refs">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj" />
<!-- Ensure PInvoke.csproj is build before Avalonia.Build.Tasks.UnitTest -->
<ProjectReference Include="..\TestFiles\BuildTasks\PInvoke\PInvoke.csproj"
SetConfiguration="Configuration=$(Configuration)"
SetTargetFramework="TargetFramework=netstandard2.0"
ReferenceOutputAssembly="false"
PrivateAssets="all" />
</ItemGroup>
</Project>

38
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);
}
}

96
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;
/// <summary>
/// This is fake BuildEngine using for testing build task
/// at moment it manage only <see cref="BuildErrorEventArgs"/> and <see cref="BuildWarningEventArgs"/>
/// other messages are ignored/>
/// </summary>
internal class UnitTestBuildEngine : IBuildEngine, IDisposable
{
private readonly bool _treatWarningAsError;
private readonly bool _assertOnDispose;
private readonly List<UnitTestBuildEngineMessage> _errors = new();
/// <summary>
/// Start new instance of <see cref="UnitTestBuildEngine"/>
/// </summary>
/// <param name="continueOnError">if it is <c>false</c> immediately assert error</param>
/// <param name="treatWarningAsError">if it is <c>true</c> treat warning as error</param>
/// <param name="assertOnDispose">if it is <c>true</c> assert on dispose if there are any errors.</param>
/// <returns></returns>
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<UnitTestBuildEngineMessage> 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);
}
}
}
}

39
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);
}

24
tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs

@ -1,5 +1,6 @@
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
@ -51,5 +52,28 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal("****", actual);
}
}
[Theory]
[InlineData(FontStretch.Condensed)]
[InlineData(FontStretch.Expanded)]
[InlineData(FontStretch.Normal)]
[InlineData(FontStretch.ExtraCondensed)]
[InlineData(FontStretch.SemiCondensed)]
[InlineData(FontStretch.ExtraExpanded)]
[InlineData(FontStretch.SemiExpanded)]
[InlineData(FontStretch.UltraCondensed)]
[InlineData(FontStretch.UltraExpanded)]
public void TextPresenter_Should_Use_FontStretch_Property(FontStretch fontStretch)
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var presenter = new TextPresenter { FontStretch = fontStretch, Text = "test" };
Assert.NotNull(presenter.TextLayout);
Assert.Equal(1, presenter.TextLayout.TextLines.Count);
Assert.Equal(1, presenter.TextLayout.TextLines[0].TextRuns.Count);
Assert.NotNull(presenter.TextLayout.TextLines[0].TextRuns[0].Properties);
Assert.Equal(fontStretch, presenter.TextLayout.TextLines[0].TextRuns[0].Properties.Typeface.Stretch);
}
}
}
}

54
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -501,6 +501,33 @@ namespace Avalonia.Controls.UnitTests
}
}
// https://github.com/AvaloniaUI/Avalonia/issues/12838
[Fact]
public void NthChild_Selector_Works_For_ItemTemplate_Children()
{
using var app = App();
var style = new Style(x => x.OfType<ContentPresenter>().NthChild(5, 0).Child().OfType<Canvas>())
{
Setters = { new Setter(Panel.BackgroundProperty, Brushes.Red) },
};
var (target, _, _) = CreateTarget(styles: new[] { style });
var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
Assert.Equal(10, realized.Count);
for (var i = 0; i < 10; ++i)
{
var container = realized[i];
var index = target.IndexFromContainer(container);
var expectedBackground = (i == 4 || i == 9) ? Brushes.Red : null;
Assert.Equal(i, index);
Assert.Equal(expectedBackground, ((Canvas) container.Child!).Background);
}
}
[Fact]
public void NthLastChild_Selector_Works()
{
@ -527,6 +554,33 @@ namespace Avalonia.Controls.UnitTests
}
}
// https://github.com/AvaloniaUI/Avalonia/issues/12838
[Fact]
public void NthLastChild_Selector_Works_For_ItemTemplate_Children()
{
using var app = App();
var style = new Style(x => x.OfType<ContentPresenter>().NthLastChild(5, 0).Child().OfType<Canvas>())
{
Setters = { new Setter(Panel.BackgroundProperty, Brushes.Red) },
};
var (target, _, _) = CreateTarget(styles: new[] { style });
var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
Assert.Equal(10, realized.Count);
for (var i = 0; i < 10; ++i)
{
var container = realized[i];
var index = target.IndexFromContainer(container);
var expectedBackground = (i == 0 || i == 5) ? Brushes.Red : null;
Assert.Equal(i, index);
Assert.Equal(expectedBackground, ((Canvas) container.Child!).Background);
}
}
[Fact]
public void ContainerPrepared_Is_Raised_When_Scrolling()
{

12
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);

78
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);
}
}
}
}
}

8
tests/TestFiles/BuildTasks/PInvoke/App.axaml

@ -0,0 +1,8 @@
<Application
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PInvoke.App">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

21
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();
}
}
}

4
tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml

@ -0,0 +1,4 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="PInvoke.MainWindow">
</Window>

22
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);
}
}

26
tests/TestFiles/BuildTasks/PInvoke/PInvoke.csproj

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
<!--<AvaloniaXamlIlDebuggerLaunch>true</AvaloniaXamlIlDebuggerLaunch>-->
<EnableAvaloniaXamlCompilation>false</EnableAvaloniaXamlCompilation>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>
<Import Project="..\..\..\..\build\SampleApp.props" />
<Import Project="..\..\..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\..\..\build\BuildTargets.targets" />
<Import Project="..\..\..\..\build\SourceGenerators.props" />
</Project>

14
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<App>()
.UsePlatformDetect()
.LogToTrace();
}
Loading…
Cancel
Save