Browse Source

Merge branch 'master' into add-scrollBarAutomationPeer

pull/10541/head
Dan Walmsley 3 years ago
committed by GitHub
parent
commit
78946a79d8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .nuke/build.schema.json
  2. 21
      Avalonia.sln
  3. 17
      build/SourceGenerators.props
  4. 7
      native/Avalonia.Native/src/OSX/AvnView.mm
  5. 24
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  6. 5
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  7. 12
      nukebuild/Build.cs
  8. 5
      nukebuild/numerge.config
  9. 6
      packages/Avalonia/Avalonia.csproj
  10. 2
      samples/ControlCatalog.NetCore/Program.cs
  11. 14
      samples/ControlCatalog/ControlCatalog.csproj
  12. 2
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  13. 8
      samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
  14. 11
      samples/ControlCatalog/Pages/LabelsPage.axaml.cs
  15. 14
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  16. 13
      samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
  17. 20
      samples/ControlCatalog/Pages/ThemePage.axaml.cs
  18. 7
      samples/Generators.Sandbox/App.xaml
  19. 20
      samples/Generators.Sandbox/App.xaml.cs
  20. 10
      samples/Generators.Sandbox/Controls/CustomTextBox.cs
  21. 45
      samples/Generators.Sandbox/Controls/SignUpView.xaml
  22. 54
      samples/Generators.Sandbox/Controls/SignUpView.xaml.cs
  23. 28
      samples/Generators.Sandbox/Generators.Sandbox.csproj
  24. 15
      samples/Generators.Sandbox/Program.cs
  25. 70
      samples/Generators.Sandbox/ViewModels/SignUpViewModel.cs
  26. 9
      samples/Generators.Sandbox/Views/SignUpView.xaml
  27. 28
      samples/Generators.Sandbox/Views/SignUpView.xaml.cs
  28. 75
      samples/IntegrationTestApp/ShowWindowTest.axaml
  29. 19
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  30. 130
      src/Avalonia.Base/Media/FontManager.cs
  31. 290
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  32. 4
      src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs
  33. 66
      src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
  34. 33
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  35. 107
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  36. 20
      src/Avalonia.Base/Media/IGlyphTypeface.cs
  37. 15
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  38. 13
      src/Avalonia.Base/Media/Typeface.cs
  39. 30
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  40. 5
      src/Avalonia.Base/Utilities/UriExtensions.cs
  41. 15
      src/Avalonia.Controls/AppBuilder.cs
  42. 9
      src/Avalonia.Controls/ItemsControl.cs
  43. 47
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  44. 90
      src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs
  45. 89
      src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs
  46. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  47. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  48. 67
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  49. 57
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs
  50. 6
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  51. 5
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
  52. 1
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
  53. 404
      src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs
  54. 13
      src/Avalonia.Fonts.Inter/AppBuilderExtension.cs
  55. 1
      src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj
  56. 14
      src/Avalonia.Fonts.Inter/InterFontCollection.cs
  57. 29
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  58. 2
      src/Avalonia.Themes.Fluent/Accents/Base.xaml
  59. 2
      src/Avalonia.Themes.Simple/Accents/Base.xaml
  60. 1
      src/Avalonia.Themes.Simple/Controls/TextBox.xaml
  61. 85
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  62. 14
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  63. 198
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  64. 73
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  65. 13
      src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs
  66. 13
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  67. 57
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  68. 20
      src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
  69. 32
      src/tools/Avalonia.Generators/Avalonia.Generators.csproj
  70. 22
      src/tools/Avalonia.Generators/Avalonia.Generators.props
  71. 9
      src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs
  72. 6
      src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs
  73. 19
      src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs
  74. 11
      src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs
  75. 18
      src/tools/Avalonia.Generators/Common/GlobPattern.cs
  76. 17
      src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs
  77. 25
      src/tools/Avalonia.Generators/Common/ResolverExtensions.cs
  78. 92
      src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs
  79. 100
      src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs
  80. 17
      src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs
  81. 50
      src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs
  82. 28
      src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs
  83. 276
      src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs
  84. 36
      src/tools/Avalonia.Generators/GeneratorContextExtensions.cs
  85. 71
      src/tools/Avalonia.Generators/GeneratorOptions.cs
  86. 63
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs
  87. 60
      src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs
  88. 11
      src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs
  89. 83
      src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs
  90. 31
      src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs
  91. 21
      src/tools/Avalonia.Generators/NameGenerator/Options.cs
  92. 8
      src/tools/Avalonia.Generators/Properties/launchSettings.json
  93. 209
      src/tools/Avalonia.Generators/README.md
  94. 6
      tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
  95. 16
      tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs
  96. 25
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  97. 61
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  98. 26
      tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj
  99. 31
      tests/Avalonia.Generators.Tests/GlobPatternTests.cs
  100. 28
      tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedProps.txt

4
.nuke/build.schema.json

@ -84,11 +84,11 @@
"GenerateCppHeaders",
"Package",
"RunCoreLibsTests",
"RunDesignerTests",
"RunHtmlPreviewerTests",
"RunLeakTests",
"RunRenderTests",
"RunTests",
"RunToolsTests",
"ZipFiles"
]
}
@ -123,11 +123,11 @@
"GenerateCppHeaders",
"Package",
"RunCoreLibsTests",
"RunDesignerTests",
"RunHtmlPreviewerTests",
"RunLeakTests",
"RunRenderTests",
"RunTests",
"RunToolsTests",
"ZipFiles"
]
}

21
Avalonia.sln

@ -244,8 +244,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -573,10 +579,22 @@ Global
{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.Build.0 = Release|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.Build.0 = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -643,7 +661,10 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

17
build/SourceGenerators.props

@ -1,5 +1,10 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PropertyGroup>
<IncludeDevGenerators Condition="'$(IncludeDevGenerators)' == ''">true</IncludeDevGenerators>
<IncludeAvaloniaGenerators Condition="'$(IncludeAvaloniaGenerators)' == ''">false</IncludeAvaloniaGenerators>
</PropertyGroup>
<ItemGroup Condition="'$(IncludeDevGenerators)' == 'true'">
<ProjectReference
Include="$(MSBuildThisFileDirectory)/../src/tools/DevGenerators/DevGenerators.csproj"
OutputItemType="Analyzer"
@ -7,4 +12,14 @@
PrivateAssets="all" />
<Compile Include="$(MSBuildThisFileDirectory)/../src/Shared/SourceGeneratorAttributes.cs" />
</ItemGroup>
<ItemGroup Condition="'$(IncludeAvaloniaGenerators)' == 'true'">
<ProjectReference
Include="../../src/tools/Avalonia.Generators/Avalonia.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
PrivateAssets="all" />
</ItemGroup>
<Import Project="$(MSBuildThisFileDirectory)/../src/tools/Avalonia.Generators/Avalonia.Generators.props"
Condition="'$(IncludeDevGenerators)' == 'true'" />
</Project>

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

@ -127,11 +127,8 @@
[self updateRenderTarget];
auto reason = [self inLiveResize] ? ResizeUser : _resizeReason;
if(_parent->IsShown())
{
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
}
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
}
}

24
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -4,6 +4,7 @@
//
#import <AppKit/AppKit.h>
#import <Cocoa/Cocoa.h>
#include "common.h"
#include "AvnView.h"
#include "menu.h"
@ -293,15 +294,24 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso
}
@try {
if(x != lastSize.width || y != lastSize.height) {
lastSize = NSSize{x, y};
if(x != lastSize.width || y != lastSize.height)
{
if (!_shown) {
BaseEvents->Resized(AvnSize{x, y}, reason);
} else if (Window != nullptr) {
[Window setContentSize:lastSize];
[Window invalidateShadow];
auto screenSize = [Window screen].visibleFrame.size;
if (x > screenSize.width) {
x = screenSize.width;
}
if (y > screenSize.height) {
y = screenSize.height;
}
}
lastSize = NSSize{x, y};
[Window setContentSize:lastSize];
[Window invalidateShadow];
}
}
@finally {

5
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -54,6 +54,11 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
WindowBaseImpl::Show(activate, isDialog);
GetWindowState(&_actualWindowState);
if(IsZoomed()) {
_lastWindowState = _actualWindowState;
}
return SetWindowState(_lastWindowState);
}
}

12
nukebuild/Build.cs

@ -220,16 +220,18 @@ partial class Build : NukeBuild
.Executes(() =>
{
RunCoreTest("Avalonia.Skia.RenderTests");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (Parameters.IsRunningOnWindows)
RunCoreTest("Avalonia.Direct2D1.RenderTests");
});
Target RunDesignerTests => _ => _
.OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
Target RunToolsTests => _ => _
.OnlyWhenStatic(() => !Parameters.SkipTests)
.DependsOn(Compile)
.Executes(() =>
{
RunCoreTest("Avalonia.DesignerSupport.Tests");
RunCoreTest("Avalonia.Generators.Tests");
if (Parameters.IsRunningOnWindows)
RunCoreTest("Avalonia.DesignerSupport.Tests");
});
Target RunLeakTests => _ => _
@ -276,7 +278,7 @@ partial class Build : NukeBuild
Target RunTests => _ => _
.DependsOn(RunCoreLibsTests)
.DependsOn(RunRenderTests)
.DependsOn(RunDesignerTests)
.DependsOn(RunToolsTests)
.DependsOn(RunHtmlPreviewerTests)
.DependsOn(RunLeakTests);

5
nukebuild/numerge.config

@ -11,6 +11,11 @@
"Id": "Avalonia.Build.Tasks",
"IgnoreMissingFrameworkBinaries": true,
"DoNotMergeDependencies": true
},
{
"Id": "Avalonia.Generators",
"IgnoreMissingFrameworkBinaries": true,
"DoNotMergeDependencies": true
}
]
}

6
packages/Avalonia/Avalonia.csproj

@ -6,11 +6,15 @@
<ItemGroup>
<ProjectReference Include="../../src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj" />
<ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj" >
<ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj">
<PrivateAssets>all</PrivateAssets>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
<SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
</ProjectReference>
<ProjectReference Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.csproj"
ReferenceOutputAssembly="false"
PrivateAssets="all"
OutputItemType="Analyzer" />
</ItemGroup>
<PropertyGroup>

2
samples/ControlCatalog.NetCore/Program.cs

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Fonts.Inter;
using Avalonia.Headless;
using Avalonia.LogicalTree;
using Avalonia.Threading;
@ -124,6 +125,7 @@ namespace ControlCatalog.NetCore
EnableIme = true
})
.UseSkia()
.WithInterFont()
.AfterSetup(builder =>
{
builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()

14
samples/ControlCatalog/ControlCatalog.csproj

@ -2,7 +2,8 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<Nullable>enable</Nullable>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
@ -35,14 +36,5 @@
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
<ItemGroup>
<None Remove="Pages\CustomDrawing.xaml" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Update="Pages\CustomDrawing.xaml">
<Generator></Generator>
</AvaloniaResource>
</ItemGroup>
<Import Project="..\..\build\SourceGenerators.props" />
</Project>

2
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Get<ComboBox>("fontComboBox");
fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
fontComboBox.Items = FontManager.Current.SystemFonts;
fontComboBox.SelectedIndex = 0;
}
}

8
samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs

@ -1,11 +1,10 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Markup.Xaml;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public class FlyoutsPage : UserControl
public partial class FlyoutsPage : UserControl
{
public FlyoutsPage()
{
@ -28,11 +27,6 @@ namespace ControlCatalog.Pages
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void SetXamlTexts()
{
var bfxt = this.Get<TextBlock>("ButtonFlyoutXamlText");

11
samples/ControlCatalog/Pages/LabelsPage.axaml.cs

@ -1,11 +1,9 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Controls;
using ControlCatalog.Models;
namespace ControlCatalog.Pages
{
public class LabelsPage : UserControl
public partial class LabelsPage : UserControl
{
private Person? _person;
@ -25,11 +23,6 @@ namespace ControlCatalog.Pages
};
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void DoSave()
{

14
samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs

@ -1,18 +1,15 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Controls;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public class RefreshContainerPage : UserControl
public partial class RefreshContainerPage : UserControl
{
private RefreshContainerViewModel _viewModel;
public RefreshContainerPage()
{
this.InitializeComponent();
InitializeComponent();
_viewModel = new RefreshContainerViewModel();
@ -27,10 +24,5 @@ namespace ControlCatalog.Pages
deferral.Complete();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

13
samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs

@ -1,19 +1,12 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Controls;
namespace ControlCatalog.Pages
{
public class RelativePanelPage : UserControl
public partial class RelativePanelPage : UserControl
{
public RelativePanelPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
InitializeComponent();
}
}
}

20
samples/ControlCatalog/Pages/ThemePage.axaml.cs

@ -1,35 +1,31 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
namespace ControlCatalog.Pages
{
public class ThemePage : UserControl
public partial class ThemePage : UserControl
{
public static ThemeVariant Pink { get; } = new("Pink", ThemeVariant.Light);
public ThemePage()
{
AvaloniaXamlLoader.Load(this);
InitializeComponent();
var selector = this.FindControl<ComboBox>("Selector")!;
var themeVariantScope = this.FindControl<ThemeVariantScope>("ThemeVariantScope")!;
selector.Items = new[]
Selector.Items = new[]
{
ThemeVariant.Default,
ThemeVariant.Dark,
ThemeVariant.Light,
Pink
};
selector.SelectedIndex = 0;
Selector.SelectedIndex = 0;
selector.SelectionChanged += (_, _) =>
Selector.SelectionChanged += (_, _) =>
{
if (selector.SelectedItem is ThemeVariant theme)
if (Selector.SelectedItem is ThemeVariant theme)
{
themeVariantScope.RequestedThemeVariant = theme;
ThemeVariantScope.RequestedThemeVariant = theme;
}
};
}

7
samples/Generators.Sandbox/App.xaml

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

20
samples/Generators.Sandbox/App.xaml.cs

@ -0,0 +1,20 @@
using Avalonia;
using Avalonia.Markup.Xaml;
using Generators.Sandbox.ViewModels;
namespace Generators.Sandbox;
public class App : Application
{
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
{
var view = new Views.SignUpView
{
ViewModel = new SignUpViewModel()
};
view.Show();
base.OnFrameworkInitializationCompleted();
}
}

10
samples/Generators.Sandbox/Controls/CustomTextBox.cs

@ -0,0 +1,10 @@
using System;
using Avalonia.Controls;
using Avalonia.Styling;
namespace Generators.Sandbox.Controls;
public class CustomTextBox : TextBox, IStyleable
{
Type IStyleable.StyleKey => typeof(TextBox);
}

45
samples/Generators.Sandbox/Controls/SignUpView.xaml

@ -0,0 +1,45 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Generators.Sandbox.Controls"
x:Class="Generators.Sandbox.Controls.SignUpView">
<StackPanel>
<controls:CustomTextBox Margin="0 10 0 0"
x:Name="UserNameTextBox"
Watermark="Please, enter user name..."
UseFloatingWatermark="True" />
<TextBlock x:Name="UserNameValidation"
Foreground="Red"
FontSize="12" />
<TextBox Margin="0 10 0 0"
x:Name="PasswordTextBox"
Watermark="Please, enter your password..."
UseFloatingWatermark="True"
PasswordChar="*" />
<TextBlock x:Name="PasswordValidation"
Foreground="Red"
FontSize="12" />
<TextBox Margin="0 10 0 0"
x:Name="ConfirmPasswordTextBox"
Watermark="Please, confirm the password..."
UseFloatingWatermark="True"
PasswordChar="*" />
<TextBlock x:Name="ConfirmPasswordValidation"
TextWrapping="Wrap"
Foreground="Red"
FontSize="12" />
<TextBlock>
<TextBlock.Inlines>
<InlineCollection>
<Run x:Name="SignUpButtonDescription" />
</InlineCollection>
</TextBlock.Inlines>
</TextBlock>
<Button Margin="0 10 0 5"
Content="Sign up"
x:Name="SignUpButton" />
<TextBlock x:Name="CompoundValidation"
TextWrapping="Wrap"
Foreground="Red"
FontSize="12" />
</StackPanel>
</UserControl>

54
samples/Generators.Sandbox/Controls/SignUpView.xaml.cs

@ -0,0 +1,54 @@
using System;
using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
using Generators.Sandbox.ViewModels;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
using ReactiveUI.Validation.Formatters;
namespace Generators.Sandbox.Controls;
/// <summary>
/// This is a sample view class with typed x:Name references generated using
/// .NET 5 source generators. The class has to be partial because x:Name
/// references are living in a separate partial class file. See also:
/// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
/// </summary>
public partial class SignUpView : ReactiveUserControl<SignUpViewModel>
{
public SignUpView()
{
// The InitializeComponent method is also generated automatically
// and lives in the autogenerated part of the partial class.
InitializeComponent();
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, x => x.UserName, x => x.UserNameTextBox.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordTextBox.Text)
.DisposeWith(disposables);
this.BindCommand(ViewModel, x => x.SignUp, x => x.SignUpButton)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordValidation.Text)
.DisposeWith(disposables);
var newLineFormatter = new SingleLineFormatter(Environment.NewLine);
this.BindValidation(ViewModel, x => x.CompoundValidation.Text, newLineFormatter)
.DisposeWith(disposables);
// The references to text boxes below are also auto generated.
// Use Ctrl+Click in order to view the generated sources.
UserNameTextBox.Text = "Joseph!";
PasswordTextBox.Text = "1234";
ConfirmPasswordTextBox.Text = "1234";
SignUpButtonDescription.Text = "Press the button below to sign up.";
});
}
}

28
samples/Generators.Sandbox/Generators.Sandbox.csproj

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="**\*.xaml"/>
<!-- Note this AdditionalFiles directive. -->
<AdditionalFiles Include="**\*.xaml"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="ReactiveUI.Validation" Version="3.0.22"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj"/>
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj"/>
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj"/>
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj"/>
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj"/>
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets"/>
<Import Project="..\..\build\SourceGenerators.props"/>
</Project>

15
samples/Generators.Sandbox/Program.cs

@ -0,0 +1,15 @@
using Avalonia;
using Avalonia.ReactiveUI;
namespace Generators.Sandbox;
internal static class Program
{
public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
private static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UseReactiveUI()
.UsePlatformDetect()
.LogToTrace();
}

70
samples/Generators.Sandbox/ViewModels/SignUpViewModel.cs

@ -0,0 +1,70 @@
using System.Reactive;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
using ReactiveUI.Validation.Helpers;
namespace Generators.Sandbox.ViewModels;
public class SignUpViewModel : ReactiveValidationObject
{
private string _userName = string.Empty;
private string _password = string.Empty;
private string _confirmPassword = string.Empty;
public SignUpViewModel()
{
this.ValidationRule(
vm => vm.UserName,
name => !string.IsNullOrWhiteSpace(name),
"UserName is required.");
this.ValidationRule(
vm => vm.Password,
password => !string.IsNullOrWhiteSpace(password),
"Password is required.");
this.ValidationRule(
vm => vm.Password,
password => password?.Length > 2,
password => $"Password should be longer, current length: {password.Length}");
this.ValidationRule(
vm => vm.ConfirmPassword,
confirmation => !string.IsNullOrWhiteSpace(confirmation),
"Confirm password field is required.");
var passwordsObservable =
this.WhenAnyValue(
x => x.Password,
x => x.ConfirmPassword,
(password, confirmation) =>
password == confirmation);
this.ValidationRule(
vm => vm.ConfirmPassword,
passwordsObservable,
"Passwords must match.");
SignUp = ReactiveCommand.Create(() => {}, this.IsValid());
}
public ReactiveCommand<Unit, Unit> SignUp { get; }
public string UserName
{
get => _userName;
set => this.RaiseAndSetIfChanged(ref _userName, value);
}
public string Password
{
get => _password;
set => this.RaiseAndSetIfChanged(ref _password, value);
}
public string ConfirmPassword
{
get => _confirmPassword;
set => this.RaiseAndSetIfChanged(ref _confirmPassword, value);
}
}

9
samples/Generators.Sandbox/Views/SignUpView.xaml

@ -0,0 +1,9 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Generators.Sandbox.Controls"
x:Class="Generators.Sandbox.Views.SignUpView">
<StackPanel Margin="10">
<TextBlock Text="Sign Up" />
<controls:SignUpView x:Name="SignUpControl" />
</StackPanel>
</Window>

28
samples/Generators.Sandbox/Views/SignUpView.xaml.cs

@ -0,0 +1,28 @@
using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
using Generators.Sandbox.ViewModels;
using ReactiveUI;
namespace Generators.Sandbox.Views;
/// <summary>
/// This is a sample view class with typed x:Name references generated using
/// .NET 5 source generators. The class has to be partial because x:Name
/// references are living in a separate partial class file. See also:
/// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
/// </summary>
public partial class SignUpView : ReactiveWindow<SignUpViewModel>
{
public SignUpView()
{
// The InitializeComponent method is also generated automatically
// and lives in the autogenerated part of the partial class.
InitializeComponent();
this.WhenActivated(disposables =>
{
this.WhenAnyValue(view => view.ViewModel)
.BindTo(this, view => view.SignUpControl.ViewModel)
.DisposeWith(disposables);
});
}
}

75
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -1,41 +1,48 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:integrationTestApp="clr-namespace:IntegrationTestApp"
x:Class="IntegrationTestApp.ShowWindowTest"
Name="SecondaryWindow"
x:DataType="Window"
Title="Show Window Test">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}"/>
<Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
<TextBox Name="CurrentFrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
Text="{Binding FrameSize, Mode=OneWay}"/>
<Label Grid.Column="0" Grid.Row="3">Position</Label>
<TextBox Name="CurrentPosition" Grid.Column="1" Grid.Row="3" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
<TextBox Name="CurrentOwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
<TextBox Name="CurrentScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="6">Scaling</Label>
<TextBox Name="CurrentScaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="7">WindowState</Label>
<ComboBox Name="CurrentWindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
<ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True"/>
<Button Name="HideButton" Grid.Row="9" Command="{Binding $parent[Window].Hide}">Hide</Button>
</Grid>
<integrationTestApp:MeasureBorder Name="MyBorder">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}" />
<Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
<TextBox Name="CurrentFrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
Text="{Binding FrameSize, Mode=OneWay}" />
<Label Grid.Column="0" Grid.Row="3">Position</Label>
<TextBox Name="CurrentPosition" Grid.Column="1" Grid.Row="3" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
<TextBox Name="CurrentOwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
<TextBox Name="CurrentScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="6">Scaling</Label>
<TextBox Name="CurrentScaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="7">WindowState</Label>
<ComboBox Name="CurrentWindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
<ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" />
<Label Grid.Row="9" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="9" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<Button Name="HideButton" Grid.Row="10" Command="{Binding $parent[Window].Hide}">Hide</Button>
</Grid>
</integrationTestApp:MeasureBorder>
</Window>

19
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@ -7,6 +7,25 @@ using Avalonia.Threading;
namespace IntegrationTestApp
{
public class MeasureBorder : Border
{
protected override Size MeasureOverride(Size availableSize)
{
MeasuredWith = availableSize;
return base.MeasureOverride(availableSize);
}
public static readonly StyledProperty<Size> MeasuredWithProperty = AvaloniaProperty.Register<MeasureBorder, Size>(
nameof(MeasuredWith));
public Size MeasuredWith
{
get => GetValue(MeasuredWithProperty);
set => SetValue(MeasuredWithProperty, value);
}
}
public class ShowWindowTest : Window
{
private readonly DispatcherTimer? _timer;

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

@ -1,9 +1,11 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media
{
@ -13,9 +15,11 @@ namespace Avalonia.Media
/// </summary>
public sealed class FontManager
{
private readonly ConcurrentDictionary<Typeface, IGlyphTypeface> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, IGlyphTypeface>();
private readonly FontFamily _defaultFontFamily;
internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts");
public const string FontCollectionScheme = "fonts";
private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
public FontManager(IFontManagerImpl platformImpl)
@ -33,9 +37,12 @@ namespace Avalonia.Media
throw new InvalidOperationException("Default font family name can't be null or empty.");
}
_defaultFontFamily = new FontFamily(DefaultFontFamilyName);
AddFontCollection(new SystemFontCollection(this));
}
/// <summary>
/// Get the current font manager instance.
/// </summary>
public static FontManager Current
{
get
@ -57,11 +64,6 @@ namespace Avalonia.Media
}
}
/// <summary>
///
/// </summary>
public IFontManagerImpl PlatformImpl { get; }
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
@ -71,41 +73,109 @@ namespace Avalonia.Media
}
/// <summary>
/// Get all installed font family names.
/// Get all system fonts.
/// </summary>
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
public IFontCollection SystemFonts => _fontCollections[SystemFontsKey];
internal IFontManagerImpl PlatformImpl { get; }
/// <summary>
/// Returns a new <see cref="IGlyphTypeface"/>, or an existing one if a matching <see cref="IGlyphTypeface"/> exists.
/// Tries to get a glyph typeface for specified typeface.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns>
/// The <see cref="IGlyphTypeface"/>.
/// <c>True</c>, if the <see cref="FontManager"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
public IGlyphTypeface GetOrAddGlyphTypeface(Typeface typeface)
public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
while (true)
glyphTypeface = null;
var fontFamily = typeface.FontFamily;
if (fontFamily.Key is FontFamilyKey key)
{
if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface))
var source = key.Source;
if (!source.IsAbsoluteUri)
{
return glyphTypeface;
if (key.BaseUri == null)
{
throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
}
source = new Uri(key.BaseUri, source);
}
glyphTypeface = PlatformImpl.CreateGlyphTypeface(typeface);
if (!_fontCollections.TryGetValue(source, out var fontCollection))
{
var embeddedFonts = new EmbeddedFontCollection(source, source);
embeddedFonts.Initialize(PlatformImpl);
if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface))
if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
{
fontCollection = embeddedFonts;
}
}
if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return glyphTypeface;
return true;
}
if (typeface.FontFamily == _defaultFontFamily)
if (!fontFamily.FamilyNames.HasFallbacks)
{
throw new InvalidOperationException($"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
return false;
}
}
typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight);
foreach (var familyName in fontFamily.FamilyNames)
{
if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return true;
}
}
return SystemFonts.TryGetGlyphTypeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface);
}
/// <summary>
/// Add a font collection to the manager.
/// </summary>
/// <param name="fontCollection">The font collection.</param>
/// <exception cref="ArgumentException"></exception>
/// <remarks>If a font collection's key is already present the collection is replaced.</remarks>
public void AddFontCollection(IFontCollection fontCollection)
{
var key = fontCollection.Key;
if (!fontCollection.Key.IsFontCollection())
{
throw new ArgumentException("Font collection Key should follow the fonts: scheme.", nameof(fontCollection));
}
_fontCollections.AddOrUpdate(key, fontCollection, (_, oldCollection) =>
{
oldCollection.Dispose();
return fontCollection;
});
fontCollection.Initialize(PlatformImpl);
}
/// <summary>
/// Removes the font collection that corresponds to specified key.
/// </summary>
/// <param name="key">The font collection's key.</param>
public void RemoveFontCollection(Uri key)
{
if (_fontCollections.TryRemove(key, out var fontCollection))
{
fontCollection.Dispose();
}
}
@ -123,18 +193,16 @@ namespace Avalonia.Media
/// <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch,
FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
{
if(_fontFallbacks != null)
if (_fontFallbacks != null)
{
foreach (var fallback in _fontFallbacks)
{
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
var glyphTypeface = GetOrAddGlyphTypeface(typeface);
if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
return true;
}
}

290
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@ -0,0 +1,290 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
public class EmbeddedFontCollection : IFontCollection
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(1);
private readonly Uri _key;
private readonly Uri _source;
public EmbeddedFontCollection(Uri key, Uri source)
{
_key = key;
_source = source;
}
public Uri Key => _key;
public FontFamily this[int index] => _fontFamilies[index];
public int Count => _fontFamilies.Count;
public void Initialize(IFontManagerImpl fontManager)
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontAssets = FontFamilyLoader.LoadFontAssets(_source);
foreach (var fontAsset in fontAssets)
{
var stream = assetLoader.Open(fontAsset);
if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface))
{
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
{
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces))
{
_fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName));
}
}
var key = new FontCollectionKey(
glyphTypeface.Style,
glyphTypeface.Weight,
glyphTypeface.Stretch);
glyphTypefaces.TryAdd(key, glyphTypeface);
}
}
}
public void Dispose()
{
foreach (var fontFamily in _fontFamilies)
{
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces))
{
foreach (var glyphTypeface in glyphTypefaces.Values)
{
glyphTypeface.Dispose();
}
}
}
GC.SuppressFinalize(this);
}
public IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
}
//Try to find a partially matching font
for (var i = 0; i < Count; i++)
{
var fontFamily = _fontFamilies[i];
if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture)))
{
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
}
}
glyphTypeface = null;
return false;
}
private static bool TryGetNearestMatch(
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
return true;
}
if (key.Style != FontStyle.Normal)
{
key = key with { Style = FontStyle.Normal };
}
if (key.Stretch != FontStretch.Normal)
{
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
if (key.Weight != FontWeight.Normal)
{
if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface))
{
return true;
}
}
key = key with { Stretch = FontStretch.Normal };
}
if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
//Take the first glyph typeface we can find.
foreach (var typeface in glyphTypefaces.Values)
{
glyphTypeface = typeface;
return true;
}
return false;
}
private static bool TryFindStretchFallback(
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
var stretch = (int)key.Stretch;
if (stretch < 5)
{
for (var i = 0; stretch + i < 9; i++)
{
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface))
{
return true;
}
}
}
else
{
for (var i = 0; stretch - i > 1; i++)
{
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface))
{
return true;
}
}
}
return false;
}
private static bool TryFindWeightFallback(
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? typeface)
{
typeface = null;
var weight = (int)key.Weight;
//If the target weight given is between 400 and 500 inclusive
if (weight >= 400 && weight <= 500)
{
//Look for available weights between the target and 500, in ascending order.
for (var i = 0; weight + i <= 500; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights greater than 500, in ascending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
}
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
if (weight < 400)
{
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
}
//If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
if (weight > 500)
{
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
}
return false;
}
}
}

4
src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs

@ -0,0 +1,4 @@
namespace Avalonia.Media.Fonts
{
public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch);
}

66
src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs

@ -11,22 +11,30 @@ namespace Avalonia.Media.Fonts
/// <summary>
/// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey) =>
IsFontTtfOrOtf(fontFamilyKey.Source) ?
GetFontAssetsByExpression(fontFamilyKey) :
GetFontAssetsBySource(fontFamilyKey);
public static IEnumerable<Uri> LoadFontAssets(Uri source)
{
if (source.IsAvares() || source.IsAbsoluteResm())
{
return IsFontTtfOrOtf(source) ?
GetFontAssetsByExpression(source) :
GetFontAssetsBySource(source);
}
return Enumerable.Empty<Uri>();
}
/// <summary>
/// Searches for font assets at a given location and returns a quantity of found assets
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsBySource(FontFamilyKey fontFamilyKey)
private static IEnumerable<Uri> GetFontAssetsBySource(Uri source)
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
var availableAssets = assetLoader.GetAssets(source, null);
return availableAssets.Where(x => IsFontTtfOrOtf(x));
}
@ -34,60 +42,50 @@ namespace Avalonia.Media.Fonts
/// Searches for font assets at a given location and only accepts assets that fit to a given filename expression.
/// <para>File names can target multiple files with * wildcard. For example "FontFile*.ttf"</para>
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
private static IEnumerable<Uri> GetFontAssetsByExpression(Uri source)
{
var (fileNameWithoutExtension, extension) = GetFileName(fontFamilyKey, out var location);
var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension);
var (fileNameWithoutExtension, extension) = GetFileName(source, out var location);
var filePattern = CreateFilePattern(source, location, fileNameWithoutExtension);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
var availableResources = assetLoader.GetAssets(location, null);
return availableResources.Where(x => IsContainsFile(x, filePattern, extension));
}
private static (string fileNameWithoutExtension, string extension) GetFileName(
FontFamilyKey fontFamilyKey, out Uri location)
Uri source, out Uri location)
{
if (fontFamilyKey.Source.IsAbsoluteResm())
if (source.IsAbsoluteResm())
{
var fileName = GetFileNameAndExtension(fontFamilyKey.Source.GetUnescapeAbsolutePath(), '.');
var fileName = GetFileNameAndExtension(source.GetUnescapeAbsolutePath(), '.');
var uriLocation = fontFamilyKey.Source.GetUnescapeAbsoluteUri()
var uriLocation = source.GetUnescapeAbsoluteUri()
.Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty);
location = new Uri(uriLocation, UriKind.RelativeOrAbsolute);
return fileName;
}
var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString);
var filename = GetFileNameAndExtension(source.OriginalString);
var fullFilename = filename.fileNameWithoutExtension + filename.extension;
if (fontFamilyKey.BaseUri != null)
{
var relativePath = fontFamilyKey.Source.OriginalString
.Replace(fullFilename, string.Empty);
location = new Uri(fontFamilyKey.BaseUri, relativePath);
}
else
{
var uriString = fontFamilyKey.Source
.GetUnescapeAbsoluteUri()
.Replace(fullFilename, string.Empty);
location = new Uri(uriString);
}
var uriString = source
.GetUnescapeAbsoluteUri()
.Replace(fullFilename, string.Empty);
location = new Uri(uriString);
return filename;
}
private static string CreateFilePattern(
FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension)
Uri source, Uri location, string fileNameWithoutExtension)
{
var path = location.GetUnescapeAbsolutePath();
var file = GetSubString(fileNameWithoutExtension, '*');
return fontFamilyKey.Source.IsAbsoluteResm()
return source.IsAbsoluteResm()
? path + "." + file
: path + file;
}

33
src/Avalonia.Base/Media/Fonts/IFontCollection.cs

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
public interface IFontCollection : IReadOnlyList<FontFamily>, IDisposable
{
/// <summary>
/// Get the font collection's key.
/// </summary>
Uri Key { get; }
/// <summary>
/// Initializes the font collection.
/// </summary>
/// <param name="fontManager">The font manager the collection is registered with.</param>
void Initialize(IFontManagerImpl fontManager);
/// <summary>
/// Try to get a glyph typeface for given parameters.
/// </summary>
/// <param name="familyName">The family name.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param>
/// <param name="stretch">The font stretch.</param>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <returns>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns>
bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
}
}

107
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@ -0,0 +1,107 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
internal class SystemFontCollection : IFontCollection
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
private readonly FontManager _fontManager;
private readonly string[] _familyNames;
public SystemFontCollection(FontManager fontManager)
{
_fontManager = fontManager;
_familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames();
}
public Uri Key => FontManager.SystemFontsKey;
public FontFamily this[int index]
{
get
{
var familyName = _familyNames[index];
return new FontFamily(familyName);
}
}
public int Count => _familyNames.Length;
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (familyName == FontFamily.DefaultFontFamilyName)
{
familyName = _fontManager.DefaultFontFamilyName;
}
var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
return true;
}
else
{
if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) &&
glyphTypefaces.TryAdd(key, glyphTypeface))
{
return true;
}
}
}
if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
{
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces))
{
return true;
}
}
return false;
}
public void Initialize(IFontManagerImpl fontManager)
{
//We initialize the system font collection during construction.
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<FontFamily> GetEnumerator()
{
foreach (var familyName in _familyNames)
{
yield return new FontFamily(familyName);
}
}
void IDisposable.Dispose()
{
foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
{
foreach (var pair in glyphTypefaces)
{
pair.Value.Dispose();
}
}
GC.SuppressFinalize(this);
}
}
}

20
src/Avalonia.Base/Media/IGlyphTypeface.cs

@ -6,6 +6,26 @@ namespace Avalonia.Media
[Unstable]
public interface IGlyphTypeface : IDisposable
{
/// <summary>
/// Gets the family name for the <see cref="IGlyphTypeface"/> object.
/// </summary>
string FamilyName { get; }
/// <summary>
/// Gets the designed weight of the font represented by the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontWeight Weight { get; }
/// <summary>
/// Gets the style for the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontStyle Style { get; }
/// <summary>
/// Gets the <see cref="FontStretch"/> value for the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontStretch Stretch { get; }
/// <summary>
/// Gets the number of glyphs held by this glyph typeface.
/// </summary>

15
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -122,13 +122,14 @@ namespace Avalonia.Media.TextFormatting
if (matchFound)
{
// Fallback found
var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
{
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
biDiLevel);
}
if(fontManager.TryGetGlyphTypeface(fallbackTypeface, out var fallbackGlyphTypeface))
{
if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
{
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
biDiLevel);
}
}
}
// no fallback found

13
src/Avalonia.Base/Media/Typeface.cs

@ -80,7 +80,18 @@ namespace Avalonia.Media
/// <value>
/// The glyph typeface.
/// </value>
public IGlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this);
public IGlyphTypeface GlyphTypeface
{
get
{
if(FontManager.Current.TryGetGlyphTypeface(this, out var glyphTypeface))
{
return glyphTypeface;
}
throw new InvalidOperationException("Could not create glyphTypeface.");
}
}
public static bool operator !=(Typeface a, Typeface b)
{

30
src/Avalonia.Base/Platform/IFontManagerImpl.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Metadata;
@ -17,7 +18,7 @@ namespace Avalonia.Platform
/// Get all installed fonts in the system.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// </summary>
IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
string[] GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
@ -37,12 +38,27 @@ namespace Avalonia.Platform
FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface);
/// <summary>
/// Creates a glyph typeface.
/// Tries to get a glyph typeface for specified parameters.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>0
/// The created glyph typeface. Can be <c>Null</c> if it was not possible to create a glyph typeface.
/// <param name="familyName">The family name.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weiht.</param>
/// <param name="stretch">The font stretch.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
/// <summary>
/// Tries to create a glyph typeface from specified stream.
/// </summary>
/// <param name="stream">A stream that holds the font's data.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
IGlyphTypeface CreateGlyphTypeface(Typeface typeface);
bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
}
}

5
src/Avalonia.Base/Utilities/UriExtensions.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Media;
namespace Avalonia.Utilities;
@ -10,7 +11,9 @@ internal static class UriExtensions
public static bool IsResm(this Uri uri) => uri.Scheme == "resm";
public static bool IsAvares(this Uri uri) => uri.Scheme == "avares";
public static bool IsFontCollection(this Uri uri) => uri.Scheme == FontManager.FontCollectionScheme;
public static Uri EnsureAbsolute(this Uri uri, Uri? baseUri)
{
if (uri.IsAbsoluteUri)

15
src/Avalonia.Controls/AppBuilder.cs

@ -4,6 +4,8 @@ using System.Reflection;
using System.Linq;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Media.Fonts;
using Avalonia.Media;
namespace Avalonia
{
@ -205,6 +207,19 @@ namespace Avalonia
return Self;
}
/// <summary>
/// Registers an action that is executed with the current font manager.
/// </summary>
/// <param name="action">The action.</param>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
public AppBuilder ConfigureFonts(Action<FontManager> action)
{
return AfterSetup(appBuilder =>
{
action?.Invoke(FontManager.Current);
});
}
/// <summary>
/// Sets up the platform-specific services for the <see cref="Application"/>.
/// </summary>

9
src/Avalonia.Controls/ItemsControl.cs

@ -383,14 +383,7 @@ namespace Avalonia.Controls
{
hic.Header = item;
hic.HeaderTemplate = itemTemplate;
itemTemplate ??= hic.FindDataTemplate(item) ?? this.FindDataTemplate(item);
if (itemTemplate is ITreeDataTemplate treeTemplate)
{
if (item is not null && treeTemplate.ItemsSelector(item) is { } itemsBinding)
BindingOperations.Apply(hic, ItemsProperty, itemsBinding, null);
}
hic.PrepareItemContainer();
}
}

47
src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs

@ -1,6 +1,8 @@
using System;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Primitives
@ -10,6 +12,9 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public class HeaderedItemsControl : ItemsControl, IContentPresenterHost
{
private IDisposable? _itemsBinding;
private bool _prepareItemContainerOnAttach;
/// <summary>
/// Defines the <see cref="Header"/> property.
/// </summary>
@ -60,6 +65,17 @@ namespace Avalonia.Controls.Primitives
/// <inheritdoc/>
IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
if (_prepareItemContainerOnAttach)
{
PrepareItemContainer();
_prepareItemContainerOnAttach = false;
}
}
/// <inheritdoc/>
bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
{
@ -81,6 +97,37 @@ namespace Avalonia.Controls.Primitives
return false;
}
internal void PrepareItemContainer()
{
_itemsBinding?.Dispose();
_itemsBinding = null;
var item = Header;
if (item is null)
{
_prepareItemContainerOnAttach = false;
return;
}
var headerTemplate = HeaderTemplate;
if (headerTemplate is null)
{
if (((ILogical)this).IsAttachedToLogicalTree)
headerTemplate = this.FindDataTemplate(item);
else
_prepareItemContainerOnAttach = true;
}
if (headerTemplate is ITreeDataTemplate treeTemplate &&
treeTemplate.Match(item) &&
treeTemplate.ItemsSelector(item) is { } itemsBinding)
{
_itemsBinding = BindingOperations.Apply(this, ItemsProperty, itemsBinding, null);
}
}
private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is ILogical oldChild)

90
src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs

@ -0,0 +1,90 @@
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Immutable;
namespace Avalonia.Diagnostics.Controls
{
internal sealed class BrushEditor : Control
{
/// <summary>
/// Defines the <see cref="Brush" /> property.
/// </summary>
public static readonly DirectProperty<BrushEditor, IBrush?> BrushProperty =
AvaloniaProperty.RegisterDirect<BrushEditor, IBrush?>(
nameof(Brush), o => o.Brush, (o, v) => o.Brush = v);
private IBrush? _brush;
public IBrush? Brush
{
get => _brush;
set => SetAndRaise(BrushProperty, ref _brush, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == BrushProperty)
{
switch (Brush)
{
case ISolidColorBrush scb:
{
var colorView = new ColorView { Color = scb.Color };
colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor);
FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = colorView });
ToolTip.SetTip(this, $"{scb.Color} ({Brush.GetType().Name})");
break;
}
default:
FlyoutBase.SetAttachedFlyout(this, null);
ToolTip.SetTip(this, Brush?.GetType().Name ?? "(null)");
break;
}
InvalidateVisual();
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
FlyoutBase.ShowAttachedFlyout(this);
}
public override void Render(DrawingContext context)
{
base.Render(context);
if (Brush != null)
{
context.FillRectangle(Brush, Bounds);
}
else
{
context.FillRectangle(Brushes.Black, Bounds);
var ft = new FormattedText("(null)",
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
Typeface.Default,
10,
Brushes.White);
context.DrawText(ft,
new Point(Bounds.Width / 2 - ft.Width / 2, Bounds.Height / 2 - ft.Height / 2));
}
}
}
}

89
src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs

@ -0,0 +1,89 @@
using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Styling;
namespace Avalonia.Diagnostics.Controls
{
//TODO: UpdateSourceTrigger & Binding.ValidationRules could help removing the need for this control.
internal sealed class CommitTextBox : TextBox, IStyleable
{
Type IStyleable.StyleKey => typeof(TextBox);
/// <summary>
/// Defines the <see cref="CommittedText" /> property.
/// </summary>
public static readonly DirectProperty<CommitTextBox, string?> CommittedTextProperty =
AvaloniaProperty.RegisterDirect<CommitTextBox, string?>(
nameof(CommittedText), o => o.CommittedText, (o, v) => o.CommittedText = v);
private string? _committedText;
public string? CommittedText
{
get => _committedText;
set => SetAndRaise(CommittedTextProperty, ref _committedText, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == CommittedTextProperty)
{
Text = CommittedText;
}
}
protected override void OnKeyUp(KeyEventArgs e)
{
base.OnKeyUp(e);
switch (e.Key)
{
case Key.Enter:
TryCommit();
e.Handled = true;
break;
case Key.Escape:
Cancel();
e.Handled = true;
break;
}
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
TryCommit();
}
private void Cancel()
{
Text = CommittedText;
DataValidationErrors.ClearErrors(this);
}
private void TryCommit()
{
if (!DataValidationErrors.GetHasErrors(this))
{
CommittedText = Text;
}
else
{
Text = CommittedText;
DataValidationErrors.ClearErrors(this);
}
}
}
}

8
src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs

@ -35,15 +35,14 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Priority => _priority;
public override Type AssignedType => _assignedType;
public override string? Value
public override object? Value
{
get => ConvertToString(_value);
get => _value;
set
{
try
{
var convertedValue = ConvertFromString(value, Property.PropertyType);
_target.SetValue(Property, convertedValue);
_target.SetValue(Property, value);
Update();
}
catch { }
@ -54,6 +53,7 @@ namespace Avalonia.Diagnostics.ViewModels
public override Type? DeclaringType { get; }
public override Type PropertyType => _propertyType;
public override bool IsReadonly => Property.IsReadOnly;
// [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update()

8
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs

@ -40,16 +40,16 @@ namespace Avalonia.Diagnostics.ViewModels
public override Type AssignedType => _assignedType;
public override Type PropertyType => _propertyType;
public override bool IsReadonly => !Property.CanWrite;
public override string? Value
public override object? Value
{
get => ConvertToString(_value);
get => _value;
set
{
try
{
var convertedValue = ConvertFromString(value, Property.PropertyType);
Property.SetValue(_target, convertedValue);
Property.SetValue(_target, value);
Update();
}
catch { }

67
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

@ -7,78 +7,21 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class PropertyViewModel : ViewModelBase
{
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
private static readonly Type[] StringParameter = { typeof(string) };
private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) };
public abstract object Key { get; }
public abstract string Name { get; }
public abstract string Group { get; }
public abstract Type AssignedType { get; }
public abstract Type? DeclaringType { get; }
public abstract string? Value { get; set; }
public abstract object? Value { get; set; }
public abstract string Priority { get; }
public abstract bool? IsAttached { get; }
public abstract void Update();
public abstract Type PropertyType { get; }
public string Type => PropertyType == AssignedType
? PropertyType.GetTypeName()
: $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}";
protected static string? ConvertToString(object? value)
{
if (value is null)
{
return "(null)";
}
var converter = TypeDescriptor.GetConverter(value);
//CollectionConverter does not deliver any important information. It just displays "(Collection)".
if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter))
{
return value.ToString() ?? "(null)";
}
return converter.ConvertToString(value);
}
private static object? InvokeParse(string s, Type targetType)
{
var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
if (method != null)
{
return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
}
method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null);
if (method != null)
{
return method.Invoke(null, new object[] { s });
}
throw new InvalidCastException("Unable to convert value.");
}
protected static object? ConvertFromString(string? s, Type targetType)
{
if (s is null)
{
return null;
}
var converter = TypeDescriptor.GetConverter(targetType);
if (converter.CanConvertFrom(typeof(string)))
{
return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
}
public string Type => PropertyType == AssignedType ?
PropertyType.GetTypeName() :
$"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}";
return InvokeParse(s, targetType);
}
public abstract bool IsReadonly { get; }
}
}

57
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs

@ -0,0 +1,57 @@
using System;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using Avalonia.Reactive;
namespace Avalonia.Diagnostics.ViewModels
{
internal static class ReactiveExtensions
{
public static IObservable<TValue> GetObservable<TOwner, TValue>(
this TOwner vm,
Expression<Func<TOwner, TValue>> property,
bool fireImmediately = true)
where TOwner : INotifyPropertyChanged
{
return Observable.Create<TValue>(o =>
{
var propertyInfo = GetPropertyInfo(property);
void Fire()
{
o.OnNext((TValue) propertyInfo.GetValue(vm)!);
}
void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == propertyInfo.Name)
{
Fire();
}
}
if (fireImmediately)
{
Fire();
}
vm.PropertyChanged += OnPropertyChanged;
return Disposable.Create(() => vm.PropertyChanged -= OnPropertyChanged);
});
}
private static PropertyInfo GetPropertyInfo<TOwner, TValue>(this Expression<Func<TOwner, TValue>> property)
{
if (property.Body is UnaryExpression unaryExpression)
{
return (PropertyInfo)((MemberExpression)unaryExpression.Operand).Member;
}
var memExpr = (MemberExpression)property.Body;
return (PropertyInfo)memExpr.Member;
}
}
}

6
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -60,7 +60,11 @@
DoubleTapped="PropertiesGrid_OnDoubleTapped">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" x:DataType="vm:PropertyViewModel" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" x:DataType="vm:PropertyViewModel" />
<DataGridTemplateColumn Header="Value" Width="100">
<DataTemplate>
<local:PropertyValueEditorView />
</DataTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Type" Binding="{Binding Type}"
IsReadOnly="True"
IsVisible="{Binding !$parent[UserControl;2].((vm:MainViewModel)DataContext).ShowDetailsPropertyType}"

5
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs

@ -1,7 +1,6 @@
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
@ -18,7 +17,7 @@ namespace Avalonia.Diagnostics.Views
public MainView()
{
InitializeComponent();
AddHandler(KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel);
AddHandler(KeyUpEvent, PreviewKeyUp);
_console = this.GetControl<ConsoleView>("console");
_consoleSplitter = this.GetControl<GridSplitter>("consoleSplitter");
_rootGrid = this.GetControl<Grid>("rootGrid");
@ -58,7 +57,7 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this);
}
private void PreviewKeyDown(object? sender, KeyEventArgs e)
private void PreviewKeyUp(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{

1
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml

@ -15,6 +15,7 @@
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml"/>
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" />
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml" />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml" />
</Window.Styles>
<Window.KeyBindings>
<KeyBinding Gesture="F8" Command="{Binding Shot}"/>

404
src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs

@ -0,0 +1,404 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Diagnostics.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Markup.Xaml.Converters;
using Avalonia.Media;
using Avalonia.Reactive;
namespace Avalonia.Diagnostics.Views
{
internal class PropertyValueEditorView : UserControl
{
private static readonly Geometry ImageIcon = Geometry.Parse(
"M12.25 6C8.79822 6 6 8.79822 6 12.25V35.75C6 37.1059 6.43174 38.3609 7.16525 39.3851L21.5252 25.0251C22.8921 23.6583 25.1081 23.6583 26.475 25.0251L40.8348 39.385C41.5683 38.3608 42 37.1058 42 35.75V12.25C42 8.79822 39.2018 6 35.75 6H12.25ZM34.5 17.5C34.5 19.7091 32.7091 21.5 30.5 21.5C28.2909 21.5 26.5 19.7091 26.5 17.5C26.5 15.2909 28.2909 13.5 30.5 13.5C32.7091 13.5 34.5 15.2909 34.5 17.5ZM39.0024 41.0881L24.7072 26.7929C24.3167 26.4024 23.6835 26.4024 23.293 26.7929L8.99769 41.0882C9.94516 41.6667 11.0587 42 12.25 42H35.75C36.9414 42 38.0549 41.6666 39.0024 41.0881Z");
private static readonly Geometry GeometryIcon = Geometry.Parse(
"M23.25 15.5H30.8529C29.8865 8.99258 24.2763 4 17.5 4C10.0442 4 4 10.0442 4 17.5C4 24.2763 8.99258 29.8865 15.5 30.8529V23.25C15.5 18.9698 18.9698 15.5 23.25 15.5ZM23.25 18C20.3505 18 18 20.3505 18 23.25V38.75C18 41.6495 20.3505 44 23.25 44H38.75C41.6495 44 44 41.6495 44 38.75V23.25C44 20.3505 41.6495 18 38.75 18H23.25Z");
private static readonly ColorToBrushConverter Color2Brush = new();
private readonly CompositeDisposable _cleanup = new();
private PropertyViewModel? Property => (PropertyViewModel?)DataContext;
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
Content = UpdateControl();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_cleanup.Clear();
}
private static bool ImplementsInterface<TInterface>(Type type)
{
var interfaceType = typeof(TInterface);
return type == interfaceType || interfaceType.IsAssignableFrom(type);
}
private Control? UpdateControl()
{
_cleanup.Clear();
if (Property?.PropertyType is not { } propertyType)
return null;
TControl CreateControl<TControl>(AvaloniaProperty valueProperty,
IValueConverter? converter = null,
Action<TControl>? init = null,
AvaloniaProperty? readonlyProperty = null)
where TControl : Control, new()
{
var control = new TControl();
init?.Invoke(control);
control.Bind(valueProperty,
new Binding(nameof(Property.Value), BindingMode.TwoWay)
{
Source = Property,
Converter = converter ?? new ValueConverter(),
ConverterParameter = propertyType
}).DisposeWith(_cleanup);
if (readonlyProperty != null)
{
control[readonlyProperty] = Property.IsReadonly;
}
else
{
control.IsEnabled = !Property.IsReadonly;
}
return control;
}
if (propertyType == typeof(bool))
return CreateControl<CheckBox>(ToggleButton.IsCheckedProperty);
//TODO: Infinity, NaN not working with NumericUpDown
if (propertyType.IsPrimitive && propertyType != typeof(float) && propertyType != typeof(double))
return CreateControl<NumericUpDown>(
NumericUpDown.ValueProperty,
new ValueToDecimalConverter(),
init: n =>
{
n.Increment = 1;
n.NumberFormat = new NumberFormatInfo { NumberDecimalDigits = 0 };
n.ParsingNumberStyle = NumberStyles.Integer;
},
readonlyProperty: NumericUpDown.IsReadOnlyProperty);
if (propertyType == typeof(Color))
{
var el = new Ellipse { Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center };
el.Bind(
Shape.FillProperty,
new Binding(nameof(Property.Value)) { Source = Property, Converter = Color2Brush })
.DisposeWith(_cleanup);
var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
tbl.Bind(
TextBlock.TextProperty,
new Binding(nameof(Property.Value)) { Source = Property })
.DisposeWith(_cleanup);
var sp = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 2,
Children = { el, tbl },
Background = Brushes.Transparent,
Cursor = new Cursor(StandardCursorType.Hand),
IsEnabled = !Property.IsReadonly
};
var cv = new ColorView();
cv.Bind(
ColorView.ColorProperty,
new Binding(nameof(Property.Value), BindingMode.TwoWay)
{
Source = Property, Converter = Color2Brush
})
.DisposeWith(_cleanup);
FlyoutBase.SetAttachedFlyout(sp, new Flyout { Content = cv });
sp.PointerPressed += (_, _) => FlyoutBase.ShowAttachedFlyout(sp);
return sp;
}
if (ImplementsInterface<IBrush>(propertyType))
return CreateControl<BrushEditor>(BrushEditor.BrushProperty);
var isImage = ImplementsInterface<IImage>(propertyType);
var isGeometry = propertyType == typeof(Geometry);
if (isImage || isGeometry)
{
var valueObservable = Property.GetObservable(x => x.Value);
var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
tbl.Bind(TextBlock.TextProperty,
valueObservable.Select(
value => value switch
{
IImage img => $"{img.Size.Width} x {img.Size.Height}",
Geometry geom => $"{geom.Bounds.Width} x {geom.Bounds.Height}",
_ => "(null)"
}))
.DisposeWith(_cleanup);
var sp = new StackPanel
{
Background = Brushes.Transparent,
Orientation = Orientation.Horizontal,
Spacing = 2,
Children =
{
new Path
{
Data = isImage ? ImageIcon : GeometryIcon,
Fill = Brushes.Gray,
Width = 12,
Height = 12,
Stretch = Stretch.Uniform,
VerticalAlignment = VerticalAlignment.Center
},
tbl
}
};
if (isImage)
{
var previewImage = new Image { Stretch = Stretch.Uniform, Width = 300, Height = 300 };
previewImage
.Bind(Image.SourceProperty, valueObservable)
.DisposeWith(_cleanup);
ToolTip.SetTip(sp, previewImage);
}
else
{
var previewShape = new Path
{
Stretch = Stretch.Uniform,
Fill = Brushes.White,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
previewShape
.Bind(Path.DataProperty, valueObservable)
.DisposeWith(_cleanup);
ToolTip.SetTip(sp, new Border { Child = previewShape, Width = 300, Height = 300 });
}
return sp;
}
if (propertyType.IsEnum)
return CreateControl<ComboBox>(
SelectingItemsControl.SelectedItemProperty, init: c =>
{
c.Items = Enum.GetValues(propertyType);
});
var tb = CreateControl<CommitTextBox>(
CommitTextBox.CommittedTextProperty,
new TextToValueConverter(),
t =>
{
t.Watermark = "(null)";
},
readonlyProperty: TextBox.IsReadOnlyProperty);
tb.IsReadOnly |= propertyType == typeof(object) ||
!StringConversionHelper.CanConvertFromString(propertyType);
if (!tb.IsReadOnly)
{
tb.GetObservable(TextBox.TextProperty).Subscribe(t =>
{
try
{
if (t != null)
{
StringConversionHelper.FromString(t, propertyType);
}
DataValidationErrors.ClearErrors(tb);
}
catch (Exception ex)
{
DataValidationErrors.SetError(tb, ex.GetBaseException());
}
}).DisposeWith(_cleanup);
}
return tb;
}
//HACK: ValueConverter that skips first target update
//TODO: Would be nice to have some kind of "InitialBindingValue" option on TwoWay bindings to control
//if the first value comes from the source or target
private class ValueConverter : IValueConverter
{
private bool _firstUpdate = true;
object? IValueConverter.Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return Convert(value, targetType, parameter, culture);
}
object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (_firstUpdate)
{
_firstUpdate = false;
return BindingOperations.DoNothing;
}
//Note: targetType provided by Converter is simply "object"
return ConvertBack(value, (Type)parameter!, parameter, culture);
}
protected virtual object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value;
}
protected virtual object? ConvertBack(object? value, Type targetType, object? parameter,
CultureInfo culture)
{
return value;
}
}
private static class StringConversionHelper
{
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
private static readonly Type[] StringParameter = { typeof(string) };
private static readonly Type[] StringFormatProviderParameters = { typeof(string), typeof(IFormatProvider) };
public static bool CanConvertFromString(Type type)
{
var converter = TypeDescriptor.GetConverter(type);
if (converter.CanConvertFrom(typeof(string)))
return true;
return GetParseMethod(type, out _) != null;
}
public static string? ToString(object o)
{
var converter = TypeDescriptor.GetConverter(o);
//CollectionConverter does not deliver any important information. It just displays "(Collection)".
if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter))
return o.ToString();
return converter.ConvertToInvariantString(o);
}
public static object? FromString(string str, Type type)
{
var converter = TypeDescriptor.GetConverter(type);
return converter.CanConvertFrom(typeof(string)) ?
converter.ConvertFrom(null, CultureInfo.InvariantCulture, str) :
InvokeParse(str, type);
}
private static object? InvokeParse(string s, Type targetType)
{
var m = GetParseMethod(targetType, out var hasFormat);
if (m == null)
throw new InvalidOperationException();
return m.Invoke(null,
hasFormat ?
new object[] { s, CultureInfo.InvariantCulture } :
new object[] { s });
}
private static MethodInfo? GetParseMethod(Type type, out bool hasFormat)
{
var m = type.GetMethod("Parse", PublicStatic, null, StringFormatProviderParameters, null);
if (m != null)
{
hasFormat = true;
return m;
}
hasFormat = false;
return type.GetMethod("Parse", PublicStatic, null, StringParameter, null);
}
}
private sealed class ValueToDecimalConverter : ValueConverter
{
protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return System.Convert.ToDecimal(value);
}
protected override object? ConvertBack(object? value, Type targetType, object? parameter,
CultureInfo culture)
{
return System.Convert.ChangeType(value, targetType);
}
}
private sealed class TextToValueConverter : ValueConverter
{
protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value is null ? null : StringConversionHelper.ToString(value);
}
protected override object? ConvertBack(object? value, Type targetType, object? parameter,
CultureInfo culture)
{
if (value is not string s)
return null;
try
{
return StringConversionHelper.FromString(s, targetType);
}
catch
{
return BindingOperations.DoNothing;
}
}
}
}
}

13
src/Avalonia.Fonts.Inter/AppBuilderExtension.cs

@ -0,0 +1,13 @@
namespace Avalonia.Fonts.Inter
{
public static class AppBuilderExtension
{
public static AppBuilder WithInterFont(this AppBuilder appBuilder)
{
return appBuilder.ConfigureFonts(fontManager =>
{
fontManager.AddFontCollection(new InterFontCollection());
});
}
}
}

1
src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj

@ -8,6 +8,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

14
src/Avalonia.Fonts.Inter/InterFontCollection.cs

@ -0,0 +1,14 @@
using System;
using Avalonia.Media.Fonts;
namespace Avalonia.Fonts.Inter
{
public sealed class InterFontCollection : EmbeddedFontCollection
{
public InterFontCollection() : base(
new Uri("fonts:Inter", UriKind.Absolute),
new Uri("avares://Avalonia.Fonts.Inter/Assets", UriKind.Absolute))
{
}
}
}

29
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -84,6 +84,14 @@ namespace Avalonia.Headless
public FontSimulations FontSimulations { get; }
public string FamilyName => "Arial";
public FontWeight Weight => FontWeight.Normal;
public FontStyle Style => FontStyle.Normal;
public FontStretch Stretch => FontStretch.Normal;
public void Dispose()
{
}
@ -147,19 +155,28 @@ namespace Avalonia.Headless
class HeadlessFontManagerStub : IFontManagerImpl
{
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public string GetDefaultFontFamilyName()
{
return new HeadlessGlyphTypefaceImpl();
return "Arial";
}
public string GetDefaultFontFamilyName()
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
return "Arial";
return new string[] { "Arial" };
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface)
{
return new List<string> { "Arial" };
glyphTypeface= new HeadlessGlyphTypefaceImpl();
return true;
}
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
glyphTypeface = new HeadlessGlyphTypefaceImpl();
return true;
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,

2
src/Avalonia.Themes.Fluent/Accents/Base.xaml

@ -3,7 +3,7 @@
xmlns:sys="using:System"
xmlns:converters="using:Avalonia.Controls.Converters">
<!-- https://docs.microsoft.com/en-us/previous-versions/windows/apps/dn518235(v=win.10)?redirectedfrom=MSDN -->
<FontFamily x:Key="ContentControlThemeFontFamily">avares://Avalonia.Fonts.Inter/Assets#Inter, $Default</FontFamily>
<FontFamily x:Key="ContentControlThemeFontFamily">fonts:Inter#Inter, $Default</FontFamily>
<sys:Double x:Key="ControlContentThemeFontSize">14</sys:Double>
<SolidColorBrush x:Key="SystemControlTransparentBrush" Color="Transparent" />

2
src/Avalonia.Themes.Simple/Accents/Base.xaml

@ -76,7 +76,7 @@
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<FontFamily x:Key="ContentControlThemeFontFamily">avares://Avalonia.Fonts.Inter/Assets#Inter, $Default</FontFamily>
<FontFamily x:Key="ContentControlThemeFontFamily">fonts://Inter#Inter, $Default</FontFamily>
<Color x:Key="ThemeAccentColor">#CC119EDA</Color>
<Color x:Key="ThemeAccentColor2">#99119EDA</Color>
<Color x:Key="ThemeAccentColor3">#66119EDA</Color>

1
src/Avalonia.Themes.Simple/Controls/TextBox.xaml

@ -92,6 +92,7 @@
<Setter Property="CaretBrush" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
<Setter Property="SelectionBrush" Value="{DynamicResource HighlightBrush}" />
<Setter Property="SelectionForegroundBrush" Value="{DynamicResource HighlightForegroundBrush}" />

85
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
@ -16,14 +17,14 @@ namespace Avalonia.Skia
return SKTypeface.Default.FamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
if (checkForUpdates)
{
_skFontManager = SKFontManager.CreateDefault();
}
return _skFontManager.FontFamilies;
return _skFontManager.GetFontFamilies();
}
[ThreadStatic] private static string[]? t_languageTagBuffer;
@ -95,72 +96,58 @@ namespace Avalonia.Skia
return false;
}
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
SKTypeface? skTypeface = null;
glyphTypeface = null;
if(typeface.FontFamily.Key is not null)
{
var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
skTypeface = fontCollection.Get(typeface);
var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch,
(SKFontStyleSlant)style);
if (skTypeface is null && !typeface.FontFamily.FamilyNames.HasFallbacks)
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
}
var skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
if (skTypeface is null)
{
var defaultName = SKTypeface.Default.FamilyName;
var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, (SKFontStyleWidth)typeface.Stretch,
(SKFontStyleSlant)typeface.Style);
foreach (var familyName in typeface.FontFamily.FamilyNames)
{
if(familyName == FontFamily.DefaultFontFamilyName)
{
continue;
}
skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
if (skTypeface is null || defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal))
{
continue;
}
break;
}
// MatchTypeface can return "null" if matched typeface wasn't found for the style
// Fallback to the default typeface and styles instead.
skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle)
?? SKTypeface.Default;
return false;
}
if (skTypeface == null)
//MatchFamily can return a font other than we requested so we have to verify we got the expected.
if (!skTypeface.FamilyName.ToLower(CultureInfo.InvariantCulture).Equals(familyName.ToLower(CultureInfo.InvariantCulture), StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
return false;
}
var fontSimulations = FontSimulations.None;
if((int)typeface.Weight >= 600 && !skTypeface.IsBold)
if ((int)weight >= 600 && !skTypeface.IsBold)
{
fontSimulations |= FontSimulations.Bold;
}
if(typeface.Style == FontStyle.Italic && !skTypeface.IsItalic)
if (style == FontStyle.Italic && !skTypeface.IsItalic)
{
fontSimulations |= FontSimulations.Oblique;
}
return new GlyphTypefaceImpl(skTypeface, fontSimulations);
glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);
return true;
}
public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
var skTypeface = SKTypeface.FromStream(stream);
if (skTypeface != null)
{
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
return true;
}
glyphTypeface = null;
return false;
}
}
}

14
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -51,6 +51,12 @@ namespace Avalonia.Skia
GlyphCount = Typeface.GlyphCount;
FontSimulations = fontSimulations;
Weight = (FontWeight)Typeface.FontWeight;
Style = Typeface.FontSlant.ToAvalonia();
Stretch = (FontStretch)Typeface.FontStyle.Width;
}
public Face Face { get; }
@ -67,6 +73,14 @@ namespace Avalonia.Skia
public int GlyphCount { get; }
public string FamilyName => Typeface.FamilyName;
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = default;

198
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -1,198 +0,0 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class SKTypefaceCollection
{
private readonly ConcurrentDictionary<Typeface, SKTypeface> _typefaces = new();
public void AddTypeface(Typeface key, SKTypeface typeface)
{
_typefaces.TryAdd(key, typeface);
}
public SKTypeface? Get(Typeface typeface)
{
return GetNearestMatch(typeface);
}
private SKTypeface? GetNearestMatch(Typeface key)
{
if (_typefaces.Count == 0)
{
return null;
}
if (_typefaces.TryGetValue(key, out var typeface))
{
return typeface;
}
if(key.Style != FontStyle.Normal)
{
key = new Typeface(key.FontFamily, FontStyle.Normal, key.Weight, key.Stretch);
}
if(key.Stretch != FontStretch.Normal)
{
if(TryFindStretchFallback(key, out typeface))
{
return typeface;
}
if(key.Weight != FontWeight.Normal)
{
if (TryFindStretchFallback(new Typeface(key.FontFamily, key.Style, FontWeight.Normal, key.Stretch), out typeface))
{
return typeface;
}
}
key = new Typeface(key.FontFamily, key.Style, key.Weight, FontStretch.Normal);
}
if(TryFindWeightFallback(key, out typeface))
{
return typeface;
}
if (TryFindStretchFallback(key, out typeface))
{
return typeface;
}
//Nothing was found so we try some regular typeface.
if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface))
{
return typeface;
}
SKTypeface? skTypeface = null;
foreach(var pair in _typefaces)
{
skTypeface = pair.Value;
if (skTypeface.FamilyName.Contains(key.FontFamily.Name))
{
return skTypeface;
}
}
return skTypeface;
}
private bool TryFindStretchFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface)
{
typeface = null;
var stretch = (int)key.Stretch;
if (stretch < 5)
{
for (var i = 0; stretch + i < 9; i++)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch + i)), out typeface))
{
return true;
}
}
}
else
{
for (var i = 0; stretch - i > 1; i++)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch - i)), out typeface))
{
return true;
}
}
}
return false;
}
private bool TryFindWeightFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface)
{
typeface = null;
var weight = (int)key.Weight;
//If the target weight given is between 400 and 500 inclusive
if (weight >= 400 && weight <= 500)
{
//Look for available weights between the target and 500, in ascending order.
for (var i = 0; weight + i <= 500; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights greater than 500, in ascending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
}
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
if (weight < 400)
{
for (var i = 0; weight - i >= 100; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
}
//If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
if (weight > 500)
{
for (var i = 0; weight + i <= 900; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
{
return true;
}
}
}
return false;
}
}
}

73
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@ -1,73 +0,0 @@
using System;
using System.Collections.Concurrent;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
{
internal static class SKTypefaceCollectionCache
{
private static readonly ConcurrentDictionary<FontFamily, SKTypefaceCollection> s_cachedCollections;
static SKTypefaceCollectionCache()
{
s_cachedCollections = new ConcurrentDictionary<FontFamily, SKTypefaceCollection>();
}
/// <summary>
/// Gets the or add typeface collection.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <returns></returns>
public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily)
{
return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection);
}
/// <summary>
/// Creates the custom font collection.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <returns></returns>
private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily)
{
var typeFaceCollection = new SKTypefaceCollection();
if (fontFamily.Key is not { } fontFamilyKey)
{
return typeFaceCollection;
}
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamilyKey);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
foreach (var asset in fontAssets)
{
var assetStream = assetLoader.Open(asset);
if (assetStream == null)
throw new InvalidOperationException("Asset could not be loaded.");
var typeface = SKTypeface.FromStream(assetStream);
if (typeface == null)
throw new InvalidOperationException("Typeface could not be loaded.");
if (!typeface.FamilyName.Contains(fontFamily.Name))
{
continue;
}
var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(),
(FontWeight)typeface.FontWeight, (FontStretch)typeface.FontWidth);
typeFaceCollection.AddTypeface(key, typeface);
}
return typeFaceCollection;
}
}
}

13
src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs

@ -1,11 +1,10 @@
using System.Collections.Generic;
using Avalonia.Platform;
using SharpDX;
using SharpDX.DirectWrite;
namespace Avalonia.Direct2D1.Media
{
using System;
using System.IO;
internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader
{
@ -18,19 +17,15 @@ namespace Avalonia.Direct2D1.Media
/// </summary>
/// <param name="factory">The factory.</param>
/// <param name="fontAssets"></param>
public DWriteResourceFontLoader(Factory factory, IEnumerable<Uri> fontAssets)
public DWriteResourceFontLoader(Factory factory, Stream[] fontAssets)
{
var factory1 = factory;
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
foreach (var asset in fontAssets)
{
var assetStream = assetLoader.Open(asset);
var dataStream = new DataStream((int)assetStream.Length, true, true);
var dataStream = new DataStream((int)asset.Length, true, true);
assetStream.CopyTo(dataStream);
asset.CopyTo(dataStream);
dataStream.Position = 0;

13
src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs

@ -6,6 +6,9 @@ using FontFamily = Avalonia.Media.FontFamily;
using FontStyle = SharpDX.DirectWrite.FontStyle;
using FontWeight = SharpDX.DirectWrite.FontWeight;
using FontStretch = SharpDX.DirectWrite.FontStretch;
using Avalonia.Platform;
using System.Linq;
using System;
namespace Avalonia.Direct2D1.Media
{
@ -53,9 +56,15 @@ namespace Avalonia.Direct2D1.Media
private static FontCollection CreateFontCollection(FontFamilyKey key)
{
var assets = FontFamilyLoader.LoadFontAssets(key);
var source = key.BaseUri != null ? new Uri(key.BaseUri, key.Source) : key.Source;
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets);
var assets = FontFamilyLoader.LoadFontAssets(source);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontAssets = assets.Select(x => assetLoader.Open(x)).ToArray();
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, fontAssets);
return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
}

57
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
using FontStretch = Avalonia.Media.FontStretch;
using FontStyle = Avalonia.Media.FontStyle;
@ -18,7 +18,7 @@ namespace Avalonia.Direct2D1.Media
return "Segoe UI";
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@ -62,9 +62,56 @@ namespace Avalonia.Direct2D1.Media
return false;
}
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
return new GlyphTypefaceImpl(typeface);
var systemFonts = Direct2D1FontCollectionCache.InstalledFontCollection;
if (familyName == FontFamily.DefaultFontFamilyName)
{
familyName = "Segoe UI";
}
if (systemFonts.FindFamilyName(familyName, out var index))
{
var font = systemFonts.GetFontFamily(index).GetFirstMatchingFont(
(SharpDX.DirectWrite.FontWeight)weight,
(SharpDX.DirectWrite.FontStretch)stretch,
(SharpDX.DirectWrite.FontStyle)style);
glyphTypeface = new GlyphTypefaceImpl(font);
return true;
}
glyphTypeface = null;
return false;
}
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, new[] { stream });
var fontCollection = new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
if (fontCollection.FontFamilyCount > 0)
{
var fontFamily = fontCollection.GetFontFamily(0);
if (fontFamily.FontCount > 0)
{
var font = fontFamily.GetFont(0);
glyphTypeface = new GlyphTypefaceImpl(font);
return true;
}
}
glyphTypeface = null;
return false;
}
}
}

20
src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs

@ -12,9 +12,9 @@ namespace Avalonia.Direct2D1.Media
{
private bool _isDisposed;
public GlyphTypefaceImpl(Typeface typeface)
public GlyphTypefaceImpl(SharpDX.DirectWrite.Font font)
{
DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
DWFont = font;
FontFace = new FontFace(DWFont).QueryInterface<FontFace1>();
@ -48,6 +48,14 @@ namespace Avalonia.Direct2D1.Media
StrikethroughThickness = strikethroughThickness,
IsFixedPitch = FontFace.IsMonospacedFont
};
FamilyName = DWFont.FontFamily.FamilyNames.GetString(0);
Weight = (Avalonia.Media.FontWeight)DWFont.Weight;
Style = (Avalonia.Media.FontStyle)DWFont.Style;
Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch;
}
private Blob GetTable(Face face, Tag tag)
@ -83,6 +91,14 @@ namespace Avalonia.Direct2D1.Media
public FontSimulations FontSimulations => FontSimulations.None;
public string FamilyName { get; }
public Avalonia.Media.FontWeight Weight { get; }
public Avalonia.Media.FontStyle Style { get; }
public Avalonia.Media.FontStretch Stretch { get; }
/// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint)
{

32
src/tools/Avalonia.Generators/Avalonia.Generators.csproj

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<PackageId>Avalonia.Generators</PackageId>
<DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants>
<IsPackable>true</IsPackable>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Compile Link="Compiler\XamlX\filename" Include="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/*.cs" />
<Compile Remove="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/SreTypeSystem.cs" />
<Compile Include="..\..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
</ItemGroup>
<ItemGroup>
<None Include="Avalonia.Generators.props" Pack="true" PackagePath="buildTransitive/$(PackageId).props" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Generators.Tests, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
<Import Project="..\..\..\build\TrimmingEnable.props" />
</Project>

22
src/tools/Avalonia.Generators/Avalonia.Generators.props

@ -0,0 +1,22 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AvaloniaNameGeneratorIsEnabled Condition="'$(AvaloniaNameGeneratorIsEnabled)' == ''">true</AvaloniaNameGeneratorIsEnabled>
<AvaloniaNameGeneratorBehavior Condition="'$(AvaloniaNameGeneratorBehavior)' == ''">InitializeComponent</AvaloniaNameGeneratorBehavior>
<AvaloniaNameGeneratorDefaultFieldModifier Condition="'$(AvaloniaNameGeneratorDefaultFieldModifier)' == ''">internal</AvaloniaNameGeneratorDefaultFieldModifier>
<AvaloniaNameGeneratorFilterByPath Condition="'$(AvaloniaNameGeneratorFilterByPath)' == ''">*</AvaloniaNameGeneratorFilterByPath>
<AvaloniaNameGeneratorFilterByNamespace Condition="'$(AvaloniaNameGeneratorFilterByNamespace)' == ''">*</AvaloniaNameGeneratorFilterByNamespace>
</PropertyGroup>
<ItemGroup>
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceItemGroup"/>
<CompilerVisibleProperty Include="AvaloniaNameGeneratorIsEnabled" />
<CompilerVisibleProperty Include="AvaloniaNameGeneratorBehavior" />
<CompilerVisibleProperty Include="AvaloniaNameGeneratorDefaultFieldModifier" />
<CompilerVisibleProperty Include="AvaloniaNameGeneratorFilterByPath" />
<CompilerVisibleProperty Include="AvaloniaNameGeneratorFilterByNamespace" />
</ItemGroup>
<Target Name="_InjectAdditionalFiles" BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun">
<ItemGroup>
<AdditionalFiles Include="@(AvaloniaXaml)" SourceItemGroup="AvaloniaXaml" />
</ItemGroup>
</Target>
</Project>

9
src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs

@ -0,0 +1,9 @@
using System.Collections.Generic;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain;
internal interface ICodeGenerator
{
string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names);
}

6
src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs

@ -0,0 +1,6 @@
namespace Avalonia.Generators.Common.Domain;
internal interface IGlobPattern
{
bool Matches(string str);
}

19
src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs

@ -0,0 +1,19 @@
using System.Collections.Generic;
using XamlX.Ast;
namespace Avalonia.Generators.Common.Domain;
internal enum NamedFieldModifier
{
Public = 0,
Private = 1,
Internal = 2,
Protected = 3,
}
internal interface INameResolver
{
IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml);
}
internal record ResolvedName(string TypeName, string Name, string FieldModifier);

11
src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs

@ -0,0 +1,11 @@
using XamlX.Ast;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain;
internal interface IViewResolver
{
ResolvedView ResolveView(string xaml);
}
internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml);

18
src/tools/Avalonia.Generators/Common/GlobPattern.cs

@ -0,0 +1,18 @@
using System.Text.RegularExpressions;
using Avalonia.Generators.Common.Domain;
namespace Avalonia.Generators.Common;
internal class GlobPattern : IGlobPattern
{
private const RegexOptions GlobOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline;
private readonly Regex _regex;
public GlobPattern(string pattern)
{
var expression = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$";
_regex = new Regex(expression, GlobOptions);
}
public bool Matches(string str) => _regex.IsMatch(str);
}

17
src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs

@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Generators.Common.Domain;
namespace Avalonia.Generators.Common;
internal class GlobPatternGroup : IGlobPattern
{
private readonly GlobPattern[] _patterns;
public GlobPatternGroup(IEnumerable<string> patterns) =>
_patterns = patterns
.Select(pattern => new GlobPattern(pattern))
.ToArray();
public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str));
}

25
src/tools/Avalonia.Generators/Common/ResolverExtensions.cs

@ -0,0 +1,25 @@
using System.Linq;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common;
internal static class ResolverExtensions
{
public static bool IsAvaloniaStyledElement(this IXamlType clrType) =>
clrType.HasStyledElementBaseType() ||
clrType.HasIStyledElementInterface();
private static bool HasStyledElementBaseType(this IXamlType clrType)
{
// Check for the base type since IStyledElement interface is removed.
// https://github.com/AvaloniaUI/Avalonia/pull/9553
if (clrType.FullName == "Avalonia.StyledElement")
return true;
return clrType.BaseType != null && IsAvaloniaStyledElement(clrType.BaseType);
}
private static bool HasIStyledElementInterface(this IXamlType clrType) =>
clrType.Interfaces.Any(abstraction =>
abstraction.IsInterface &&
abstraction.FullName == "Avalonia.IStyledElement");
}

92
src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs

@ -0,0 +1,92 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Avalonia.Generators.Common.Domain;
using XamlX;
using XamlX.Ast;
namespace Avalonia.Generators.Common;
internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
{
private readonly List<ResolvedName> _items = new();
private readonly string _defaultFieldModifier;
public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal)
{
_defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant();
}
public IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml)
{
_items.Clear();
xaml.Root.Visit(this);
xaml.Root.VisitChildren(this);
return _items;
}
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{
if (node is not XamlAstObjectNode objectNode)
return node;
var clrType = objectNode.Type.GetClrType();
if (!clrType.IsAvaloniaStyledElement())
return node;
foreach (var child in objectNode.Children)
{
if (child is XamlAstXamlPropertyValueNode propertyValueNode &&
propertyValueNode.Property is XamlAstNamePropertyReference namedProperty &&
namedProperty.Name == "Name" &&
propertyValueNode.Values.Count > 0 &&
propertyValueNode.Values[0] is XamlAstTextNode text)
{
var fieldModifier = TryGetFieldModifier(objectNode);
var typeName = $@"{clrType.Namespace}.{clrType.Name}";
var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList();
var genericTypeName = typeAgs.Count == 0
? $"global::{typeName}"
: $@"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>";
var resolvedName = new ResolvedName(genericTypeName, text.Text, fieldModifier);
if (_items.Contains(resolvedName))
continue;
_items.Add(resolvedName);
}
}
return node;
}
void IXamlAstVisitor.Push(IXamlAstNode node) { }
void IXamlAstVisitor.Pop() { }
private string TryGetFieldModifier(XamlAstObjectNode objectNode)
{
// We follow Xamarin.Forms API behavior in terms of x:FieldModifier here:
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers
// However, by default we use 'internal' field modifier here for generated
// x:Name references for historical purposes and WPF compatibility.
//
var fieldModifierType = objectNode
.Children
.OfType<XamlAstXmlDirective>()
.Where(dir => dir.Name == "FieldModifier" && dir.Namespace == XamlNamespaces.Xaml2006)
.Select(dir => dir.Values[0])
.OfType<XamlAstTextNode>()
.Select(txt => txt.Text)
.FirstOrDefault();
return fieldModifierType?.ToLowerInvariant() switch
{
"private" => "private",
"public" => "public",
"protected" => "protected",
"internal" => "internal",
"notpublic" => "internal",
_ => _defaultFieldModifier
};
}
}

100
src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler;
using XamlX;
using XamlX.Ast;
using XamlX.Parsers;
namespace Avalonia.Generators.Common;
internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor
{
private readonly RoslynTypeSystem _typeSystem;
private readonly MiniCompiler _compiler;
private readonly bool _checkTypeValidity;
private readonly Action<string> _onTypeInvalid;
private readonly Action<Exception> _onUnhandledError;
private ResolvedView _resolvedClass;
private XamlDocument _xaml;
public XamlXViewResolver(
RoslynTypeSystem typeSystem,
MiniCompiler compiler,
bool checkTypeValidity = false,
Action<string> onTypeInvalid = null,
Action<Exception> onUnhandledError = null)
{
_checkTypeValidity = checkTypeValidity;
_onTypeInvalid = onTypeInvalid;
_onUnhandledError = onUnhandledError;
_typeSystem = typeSystem;
_compiler = compiler;
}
public ResolvedView ResolveView(string xaml)
{
try
{
_resolvedClass = null;
_xaml = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>
{
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008}
});
_compiler.Transform(_xaml);
_xaml.Root.Visit(this);
_xaml.Root.VisitChildren(this);
return _resolvedClass;
}
catch (Exception exception)
{
_onUnhandledError?.Invoke(exception);
return null;
}
}
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{
if (node is not XamlAstObjectNode objectNode)
return node;
var clrType = objectNode.Type.GetClrType();
if (!clrType.IsAvaloniaStyledElement())
return node;
foreach (var child in objectNode.Children)
{
if (child is XamlAstXmlDirective directive &&
directive.Name == "Class" &&
directive.Namespace == XamlNamespaces.Xaml2006 &&
directive.Values[0] is XamlAstTextNode text)
{
if (_checkTypeValidity)
{
var existingType = _typeSystem.FindType(text.Text);
if (existingType == null)
{
_onTypeInvalid?.Invoke(text.Text);
return node;
}
}
var split = text.Text.Split('.');
var nameSpace = string.Join(".", split.Take(split.Length - 1));
var className = split.Last();
_resolvedClass = new ResolvedView(className, clrType, nameSpace, _xaml);
return node;
}
}
return node;
}
void IXamlAstVisitor.Push(IXamlAstNode node) { }
void IXamlAstVisitor.Pop() { }
}

17
src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs

@ -0,0 +1,17 @@
using XamlX.Ast;
using XamlX.Transform;
namespace Avalonia.Generators.Compiler;
internal class DataTemplateTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is XamlAstObjectNode objectNode &&
objectNode.Type is XamlAstXmlTypeReference typeReference &&
(typeReference.Name == "DataTemplate" ||
typeReference.Name == "ControlTemplate"))
objectNode.Children.Clear();
return node;
}
}

50
src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using XamlX.Compiler;
using XamlX.Emit;
using XamlX.Transform;
using XamlX.Transform.Transformers;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Compiler;
internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
{
public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute";
public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes)
{
var mappings = new XamlLanguageTypeMappings(typeSystem);
foreach (var additionalType in additionalTypes)
mappings.XmlnsAttributes.Add(typeSystem.GetType(additionalType));
var configuration = new TransformerConfiguration(
typeSystem,
typeSystem.Assemblies.First(),
mappings);
return new MiniCompiler(configuration);
}
private MiniCompiler(TransformerConfiguration configuration)
: base(configuration, new XamlLanguageEmitMappings<object, IXamlEmitResult>(), false)
{
Transformers.Add(new NameDirectiveTransformer());
Transformers.Add(new DataTemplateTransformer());
Transformers.Add(new KnownDirectivesTransformer());
Transformers.Add(new XamlIntrinsicsTransformer());
Transformers.Add(new XArgumentsTransformer());
Transformers.Add(new TypeReferenceResolver());
}
protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen(
IFileSource file,
Func<string, IXamlType,
IXamlTypeBuilder<object>> createSubType,
Func<string, IXamlType, IEnumerable<IXamlType>,
IXamlTypeBuilder<object>> createDelegateType,
object codeGen,
XamlRuntimeContext<object, IXamlEmitResult> context,
bool needContextLocal) =>
throw new NotSupportedException();
}

28
src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs

@ -0,0 +1,28 @@
using XamlX;
using XamlX.Ast;
using XamlX.Transform;
namespace Avalonia.Generators.Compiler;
internal class NameDirectiveTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is not XamlAstObjectNode objectNode)
return node;
for (var index = 0; index < objectNode.Children.Count; index++)
{
var child = objectNode.Children[index];
if (child is XamlAstXmlDirective directive &&
directive.Namespace == XamlNamespaces.Xaml2006 &&
directive.Name == "Name")
objectNode.Children[index] = new XamlAstXamlPropertyValueNode(
directive,
new XamlAstNamePropertyReference(directive, objectNode.Type, "Name", objectNode.Type),
directive.Values);
}
return node;
}
}

276
src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs

@ -0,0 +1,276 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Compiler;
internal class RoslynTypeSystem : IXamlTypeSystem
{
private readonly List<IXamlAssembly> _assemblies = new();
public RoslynTypeSystem(CSharpCompilation compilation)
{
_assemblies.Add(new RoslynAssembly(compilation.Assembly));
var assemblySymbols = compilation
.References
.Select(compilation.GetAssemblyOrModuleSymbol)
.OfType<IAssemblySymbol>()
.Select(assembly => new RoslynAssembly(assembly))
.ToList();
_assemblies.AddRange(assemblySymbols);
}
public IEnumerable<IXamlAssembly> Assemblies => _assemblies;
public IXamlAssembly FindAssembly(string name) =>
Assemblies
.FirstOrDefault(a => string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase));
public IXamlType FindType(string name) =>
_assemblies
.Select(assembly => assembly.FindType(name))
.FirstOrDefault(type => type != null);
public IXamlType FindType(string name, string assembly) =>
_assemblies
.Select(assemblyInstance => assemblyInstance.FindType(name))
.FirstOrDefault(type => type != null);
}
internal class RoslynAssembly : IXamlAssembly
{
private readonly IAssemblySymbol _symbol;
public RoslynAssembly(IAssemblySymbol symbol) => _symbol = symbol;
public bool Equals(IXamlAssembly other) =>
other is RoslynAssembly roslynAssembly &&
SymbolEqualityComparer.Default.Equals(_symbol, roslynAssembly._symbol);
public string Name => _symbol.Name;
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes =>
_symbol.GetAttributes()
.Select(data => new RoslynAttribute(data, this))
.ToList();
public IXamlType FindType(string fullName)
{
var type = _symbol.GetTypeByMetadataName(fullName);
return type is null ? null : new RoslynType(type, this);
}
}
internal class RoslynAttribute : IXamlCustomAttribute
{
private readonly AttributeData _data;
private readonly RoslynAssembly _assembly;
public RoslynAttribute(AttributeData data, RoslynAssembly assembly)
{
_data = data;
_assembly = assembly;
}
public bool Equals(IXamlCustomAttribute other) =>
other is RoslynAttribute attribute &&
_data == attribute._data;
public IXamlType Type => new RoslynType(_data.AttributeClass, _assembly);
public List<object> Parameters =>
_data.ConstructorArguments
.Select(argument => argument.Value)
.ToList();
public Dictionary<string, object> Properties =>
_data.NamedArguments.ToDictionary(
pair => pair.Key,
pair => pair.Value.Value);
}
internal class RoslynType : IXamlType
{
private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters |
SymbolDisplayGenericsOptions.IncludeTypeConstraints |
SymbolDisplayGenericsOptions.IncludeVariance);
private readonly RoslynAssembly _assembly;
private readonly INamedTypeSymbol _symbol;
public RoslynType(INamedTypeSymbol symbol, RoslynAssembly assembly)
{
_symbol = symbol;
_assembly = assembly;
}
public bool Equals(IXamlType other) =>
other is RoslynType roslynType &&
SymbolEqualityComparer.Default.Equals(_symbol, roslynType._symbol);
public object Id => _symbol;
public string Name => _symbol.Name;
public string Namespace => _symbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat);
public string FullName => $"{Namespace}.{Name}";
public IXamlAssembly Assembly => _assembly;
public IReadOnlyList<IXamlProperty> Properties =>
_symbol.GetMembers()
.Where(member => member.Kind == SymbolKind.Property)
.OfType<IPropertySymbol>()
.Select(property => new RoslynProperty(property, _assembly))
.ToList();
public IReadOnlyList<IXamlEventInfo> Events { get; } = new List<IXamlEventInfo>();
public IReadOnlyList<IXamlField> Fields { get; } = new List<IXamlField>();
public IReadOnlyList<IXamlMethod> Methods { get; } = new List<IXamlMethod>();
public IReadOnlyList<IXamlConstructor> Constructors =>
_symbol.Constructors
.Select(method => new RoslynConstructor(method, _assembly))
.ToList();
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>();
public IReadOnlyList<IXamlType> GenericArguments { get; private set; } = new List<IXamlType>();
public bool IsAssignableFrom(IXamlType type) => type == this;
public IXamlType MakeGenericType(IReadOnlyList<IXamlType> typeArguments)
{
GenericArguments = typeArguments;
return this;
}
public IXamlType GenericTypeDefinition => this;
public bool IsArray => false;
public IXamlType ArrayElementType { get; } = null;
public IXamlType MakeArrayType(int dimensions) => null;
public IXamlType BaseType => _symbol.BaseType == null ? null : new RoslynType(_symbol.BaseType, _assembly);
public bool IsValueType { get; } = false;
public bool IsEnum { get; } = false;
public IReadOnlyList<IXamlType> Interfaces =>
_symbol.AllInterfaces
.Select(abstraction => new RoslynType(abstraction, _assembly))
.ToList();
public bool IsInterface => _symbol.IsAbstract;
public IXamlType GetEnumUnderlyingType() => null;
public IReadOnlyList<IXamlType> GenericParameters { get; } = new List<IXamlType>();
}
internal class RoslynConstructor : IXamlConstructor
{
private readonly IMethodSymbol _symbol;
private readonly RoslynAssembly _assembly;
public RoslynConstructor(IMethodSymbol symbol, RoslynAssembly assembly)
{
_symbol = symbol;
_assembly = assembly;
}
public bool Equals(IXamlConstructor other) =>
other is RoslynConstructor roslynConstructor &&
SymbolEqualityComparer.Default.Equals(_symbol, roslynConstructor._symbol);
public bool IsPublic => true;
public bool IsStatic => false;
public IReadOnlyList<IXamlType> Parameters =>
_symbol.Parameters
.Select(parameter => parameter.Type)
.OfType<INamedTypeSymbol>()
.Select(type => new RoslynType(type, _assembly))
.ToList();
}
internal class RoslynProperty : IXamlProperty
{
private readonly IPropertySymbol _symbol;
private readonly RoslynAssembly _assembly;
public RoslynProperty(IPropertySymbol symbol, RoslynAssembly assembly)
{
_symbol = symbol;
_assembly = assembly;
}
public bool Equals(IXamlProperty other) =>
other is RoslynProperty roslynProperty &&
SymbolEqualityComparer.Default.Equals(_symbol, roslynProperty._symbol);
public string Name => _symbol.Name;
public IXamlType PropertyType =>
_symbol.Type is INamedTypeSymbol namedTypeSymbol
? new RoslynType(namedTypeSymbol, _assembly)
: null;
public IXamlMethod Getter => _symbol.GetMethod == null ? null : new RoslynMethod(_symbol.GetMethod, _assembly);
public IXamlMethod Setter => _symbol.SetMethod == null ? null : new RoslynMethod(_symbol.SetMethod, _assembly);
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>();
public IReadOnlyList<IXamlType> IndexerParameters { get; } = new List<IXamlType>();
}
internal class RoslynMethod : IXamlMethod
{
private readonly IMethodSymbol _symbol;
private readonly RoslynAssembly _assembly;
public RoslynMethod(IMethodSymbol symbol, RoslynAssembly assembly)
{
_symbol = symbol;
_assembly = assembly;
}
public bool Equals(IXamlMethod other) =>
other is RoslynMethod roslynMethod &&
SymbolEqualityComparer.Default.Equals(roslynMethod._symbol, _symbol);
public string Name => _symbol.Name;
public bool IsPublic => true;
public bool IsStatic => false;
public IXamlType ReturnType => new RoslynType((INamedTypeSymbol) _symbol.ReturnType, _assembly);
public IReadOnlyList<IXamlType> Parameters =>
_symbol.Parameters.Select(parameter => parameter.Type)
.OfType<INamedTypeSymbol>()
.Select(type => new RoslynType(type, _assembly))
.ToList();
public IXamlType DeclaringType => new RoslynType((INamedTypeSymbol)_symbol.ReceiverType, _assembly);
public IXamlMethod MakeGenericMethod(IReadOnlyList<IXamlType> typeArguments) => null;
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>();
}

36
src/tools/Avalonia.Generators/GeneratorContextExtensions.cs

@ -0,0 +1,36 @@
using System;
using Microsoft.CodeAnalysis;
namespace Avalonia.Generators;
internal static class GeneratorContextExtensions
{
private const string UnhandledErrorDescriptorId = "AXN0002";
private const string InvalidTypeDescriptorId = "AXN0001";
public static string GetMsBuildProperty(
this GeneratorExecutionContext context,
string name,
string defaultValue = "")
{
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value);
return value ?? defaultValue;
}
public static void ReportNameGeneratorUnhandledError(this GeneratorExecutionContext context, Exception error) =>
context.Report(UnhandledErrorDescriptorId,
"Unhandled exception occured while generating typed Name references. " +
"Please file an issue: https://github.com/avaloniaui/Avalonia.Generators",
error.ToString());
public static void ReportNameGeneratorInvalidType(this GeneratorExecutionContext context, string typeName) =>
context.Report(InvalidTypeDescriptorId,
$"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " +
$"The type '{typeName}' does not exist in the assembly.");
private static void Report(this GeneratorExecutionContext context, string id, string title, string message = null) =>
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(id, title, message ?? title, "Usage", DiagnosticSeverity.Error, true),
Location.None));
}

71
src/tools/Avalonia.Generators/GeneratorOptions.cs

@ -0,0 +1,71 @@
using System;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.NameGenerator;
using Microsoft.CodeAnalysis;
namespace Avalonia.Generators;
// When update these enum values, don't forget to update Avalonia.Generators.props.
internal enum BuildProperties
{
AvaloniaNameGeneratorIsEnabled = 0,
AvaloniaNameGeneratorBehavior = 1,
AvaloniaNameGeneratorDefaultFieldModifier = 2,
AvaloniaNameGeneratorFilterByPath = 3,
AvaloniaNameGeneratorFilterByNamespace = 4,
AvaloniaNameGeneratorViewFileNamingStrategy = 5,
// TODO add other generators properties here.
}
internal class GeneratorOptions
{
private readonly GeneratorExecutionContext _context;
public GeneratorOptions(GeneratorExecutionContext context) => _context = context;
public bool AvaloniaNameGeneratorIsEnabled => GetBoolProperty(
BuildProperties.AvaloniaNameGeneratorIsEnabled,
true);
public Behavior AvaloniaNameGeneratorBehavior => GetEnumProperty(
BuildProperties.AvaloniaNameGeneratorBehavior,
Behavior.InitializeComponent);
public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier => GetEnumProperty(
BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier,
NamedFieldModifier.Internal);
public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy => GetEnumProperty(
BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy,
ViewFileNamingStrategy.NamespaceAndClassName);
public string[] AvaloniaNameGeneratorFilterByPath => GetStringArrayProperty(
BuildProperties.AvaloniaNameGeneratorFilterByPath,
"*");
public string[] AvaloniaNameGeneratorFilterByNamespace => GetStringArrayProperty(
BuildProperties.AvaloniaNameGeneratorFilterByNamespace,
"*");
private string[] GetStringArrayProperty(BuildProperties name, string defaultValue)
{
var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue);
return value.Contains(";") ? value.Split(';') : new[] {value};
}
private TEnum GetEnumProperty<TEnum>(BuildProperties name, TEnum defaultValue) where TEnum : struct
{
var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue.ToString());
return Enum.TryParse(value, true, out TEnum behavior) ? behavior : defaultValue;
}
private bool GetBoolProperty(BuildProperties name, bool defaultValue)
{
var key = name.ToString();
var value = _context.GetMsBuildProperty(key, defaultValue.ToString());
return bool.TryParse(value, out var result) ? result : defaultValue;
}
}

63
src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Generators.Common.Domain;
using Microsoft.CodeAnalysis;
namespace Avalonia.Generators.NameGenerator;
internal class AvaloniaNameGenerator : INameGenerator
{
private readonly ViewFileNamingStrategy _naming;
private readonly IGlobPattern _pathPattern;
private readonly IGlobPattern _namespacePattern;
private readonly IViewResolver _classes;
private readonly INameResolver _names;
private readonly ICodeGenerator _code;
public AvaloniaNameGenerator(
ViewFileNamingStrategy naming,
IGlobPattern pathPattern,
IGlobPattern namespacePattern,
IViewResolver classes,
INameResolver names,
ICodeGenerator code)
{
_naming = naming;
_pathPattern = pathPattern;
_namespacePattern = namespacePattern;
_classes = classes;
_names = names;
_code = code;
}
public IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles)
{
var resolveViews =
from file in additionalFiles
where (file.Path.EndsWith(".xaml") ||
file.Path.EndsWith(".paml") ||
file.Path.EndsWith(".axaml")) &&
_pathPattern.Matches(file.Path)
let xaml = file.GetText()!.ToString()
let view = _classes.ResolveView(xaml)
where view != null && _namespacePattern.Matches(view.Namespace)
select view;
var query =
from view in resolveViews
let names = _names.ResolveNames(view.Xaml)
let code = _code.GenerateCode(view.ClassName, view.Namespace, view.XamlType, names)
let fileName = ResolveViewFileName(view, _naming)
select new GeneratedPartialClass(fileName, code);
return query.ToList();
}
private static string ResolveViewFileName(ResolvedView view, ViewFileNamingStrategy strategy) => strategy switch
{
ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs",
ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs",
_ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, "Unknown naming strategy!")
};
}

60
src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs

@ -0,0 +1,60 @@
using System;
using Avalonia.Generators.Common;
using Avalonia.Generators.Common.Domain;
using Avalonia.Generators.Compiler;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace Avalonia.Generators.NameGenerator;
[Generator]
public class AvaloniaNameSourceGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
try
{
var generator = CreateNameGenerator(context);
if (generator is null)
{
return;
}
var partials = generator.GenerateNameReferences(context.AdditionalFiles);
foreach (var (fileName, content) in partials) context.AddSource(fileName, content);
}
catch (Exception exception)
{
context.ReportNameGeneratorUnhandledError(exception);
}
}
private static INameGenerator CreateNameGenerator(GeneratorExecutionContext context)
{
var options = new GeneratorOptions(context);
if (!options.AvaloniaNameGeneratorIsEnabled)
{
return null;
}
var types = new RoslynTypeSystem((CSharpCompilation)context.Compilation);
ICodeGenerator generator = options.AvaloniaNameGeneratorBehavior switch {
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(),
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types),
_ => throw new ArgumentOutOfRangeException()
};
var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute);
return new AvaloniaNameGenerator(
options.AvaloniaNameGeneratorViewFileNamingStrategy,
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByPath),
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByNamespace),
new XamlXViewResolver(types, compiler, true,
type => context.ReportNameGeneratorInvalidType(type),
error => context.ReportNameGeneratorUnhandledError(error)),
new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier),
generator);
}
}

11
src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs

@ -0,0 +1,11 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
namespace Avalonia.Generators.NameGenerator;
internal interface INameGenerator
{
IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles);
}
internal record GeneratedPartialClass(string FileName, string Content);

83
src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs

@ -0,0 +1,83 @@
using System.Collections.Generic;
using Avalonia.Generators.Common.Domain;
using XamlX.TypeSystem;
namespace Avalonia.Generators.NameGenerator;
internal class InitializeComponentCodeGenerator: ICodeGenerator
{
private readonly bool _diagnosticsAreConnected;
private const string AttachDevToolsCodeBlock = @"
#if DEBUG
if (attachDevTools)
{
this.AttachDevTools();
}
#endif
";
private const string AttachDevToolsParameterDocumentation
= @" /// <param name=""attachDevTools"">Should the dev tools be attached.</param>
";
public InitializeComponentCodeGenerator(IXamlTypeSystem types)
{
_diagnosticsAreConnected = types.FindAssembly("Avalonia.Diagnostics") != null;
}
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names)
{
var properties = new List<string>();
var initializations = new List<string>();
foreach (var resolvedName in names)
{
var (typeName, name, fieldModifier) = resolvedName;
properties.Add($" {fieldModifier} {typeName} {name};");
initializations.Add($" {name} = this.FindNameScope()?.Find<{typeName}>(\"{name}\");");
}
var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType);
return $@"// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace {nameSpace}
{{
partial class {className}
{{
{string.Join("\n", properties)}
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name=""loadXaml"">Should the XAML be loaded into the component.</param>
{(attachDevTools ? AttachDevToolsParameterDocumentation : string.Empty)}
public void InitializeComponent(bool loadXaml = true{(attachDevTools ? ", bool attachDevTools = true" : string.Empty)})
{{
if (loadXaml)
{{
AvaloniaXamlLoader.Load(this);
}}
{(attachDevTools ? AttachDevToolsCodeBlock : string.Empty)}
{string.Join("\n", initializations)}
}}
}}
}}
";
}
private static bool IsWindow(IXamlType xamlType)
{
var type = xamlType;
bool isWindow;
do
{
isWindow = type.FullName == "Avalonia.Controls.Window";
type = type.BaseType;
} while (!isWindow && type != null);
return isWindow;
}
}

31
src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Generators.Common.Domain;
using XamlX.TypeSystem;
namespace Avalonia.Generators.NameGenerator;
internal class OnlyPropertiesCodeGenerator : ICodeGenerator
{
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names)
{
var namedControls = names
.Select(info => " " +
$"{info.FieldModifier} {info.TypeName} {info.Name} => " +
$"this.FindNameScope()?.Find<{info.TypeName}>(\"{info.Name}\");")
.ToList();
var lines = string.Join("\n", namedControls);
return $@"// <auto-generated />
using Avalonia.Controls;
namespace {nameSpace}
{{
partial class {className}
{{
{lines}
}}
}}
";
}
}

21
src/tools/Avalonia.Generators/NameGenerator/Options.cs

@ -0,0 +1,21 @@
namespace Avalonia.Generators.NameGenerator;
internal enum Options
{
Public = 0,
Private = 1,
Internal = 2,
Protected = 3,
}
internal enum Behavior
{
OnlyProperties = 0,
InitializeComponent = 1,
}
internal enum ViewFileNamingStrategy
{
ClassName = 0,
NamespaceAndClassName = 1,
}

8
src/tools/Avalonia.Generators/Properties/launchSettings.json

@ -0,0 +1,8 @@
{
"profiles": {
"Profile 1": {
"commandName": "DebugRoslynComponent",
"targetProject": "..\\..\\..\\samples\\Generators.Sandbox\\Generators.Sandbox.csproj"
}
}
}

209
src/tools/Avalonia.Generators/README.md

@ -0,0 +1,209 @@
[![NuGet Stats](https://img.shields.io/nuget/v/XamlNameReferenceGenerator.svg)](https://www.nuget.org/packages/XamlNameReferenceGenerator) [![downloads](https://img.shields.io/nuget/dt/XamlNameReferenceGenerator)](https://www.nuget.org/packages/XamlNameReferenceGenerator) ![Build](https://github.com/avaloniaui/Avalonia.NameGenerator/workflows/Build/badge.svg) ![License](https://img.shields.io/github/license/avaloniaui/Avalonia.NameGenerator.svg) ![Size](https://img.shields.io/github/repo-size/avaloniaui/Avalonia.NameGenerator.svg)
### C# `SourceGenerator` for Typed Avalonia `x:Name` References
This is a [C# `SourceGenerator`](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) built for generating strongly-typed references to controls with `x:Name` (or just `Name`) attributes declared in XAML (or, in `.axaml`). The source generator will look for the `xaml` (or `axaml`) file with the same name as your partial C# class that is a subclass of `Avalonia.INamed` and parses the XAML markup, finds all XAML tags with `x:Name` attributes and generates the C# code.
### Getting Started
In order to get started, just install the NuGet package:
```
dotnet add package XamlNameReferenceGenerator
```
Or, if you are using [submodules](https://git-scm.com/docs/git-submodule), you can reference the generator as such:
```xml
<ItemGroup>
<!-- Remember to ensure XAML files are included via <AdditionalFiles>,
otherwise C# source generator won't see XAML files. -->
<AdditionalFiles Include="**\*.xaml"/>
<ProjectReference Include="..\Avalonia.NameGenerator\Avalonia.NameGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
```
### Usage
After installing the NuGet package, declare your view class as `partial`. Typed C# references to Avalonia controls declared in XAML files will be generated for classes referenced by the `x:Class` directive in XAML files. For example, for the following XAML markup:
```xml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.SignUpView">
<TextBox x:Name="UserNameTextBox" x:FieldModifier="public" />
</Window>
```
A new C# partial class named `SignUpView` with a single `public` property named `UserNameTextBox` of type `TextBox` will be generated in the `Sample.App` namespace. We won't see the generated file, but we'll be able to access the generated property as shown below:
```cs
using Avalonia.Controls;
namespace Sample.App
{
public partial class SignUpView : Window
{
public SignUpView()
{
// This method is generated. Call it before accessing any
// of the generated properties. The 'UserNameTextBox'
// property is also generated.
InitializeComponent();
UserNameTextBox.Text = "Joseph";
}
}
}
```
<img src="https://hsto.org/getpro/habr/post_images/d9f/4aa/a1e/d9f4aaa1eb450f5dd2fca66631bc16a0.gif" />
### Why do I need this?
The typed `x:Name` references might be useful if you decide to use e.g. [ReactiveUI code-behind bindings](https://www.reactiveui.net/docs/handbook/data-binding/):
```cs
// UserNameValidation and PasswordValidation are auto generated.
public partial class SignUpView : ReactiveWindow<SignUpViewModel>
{
public SignUpView()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text)
.DisposeWith(disposables);
this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text)
.DisposeWith(disposables);
});
}
}
```
### Advanced Usage
> Never keep a method named `InitializeComponent` in your code-behind view class if you are using the generator with `AvaloniaNameGeneratorBehavior` set to `InitializeComponent` (this is the default value). The private `InitializeComponent` method declared in your code-behind class hides the `InitializeComponent` method generated by `Avalonia.NameGenerator`, see [Issue 69](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/69). If you wish to use your own `InitializeComponent` method (not the generated one), set `AvaloniaNameGeneratorBehavior` to `OnlyProperties`.
The `x:Name` generator can be configured via MsBuild properties that you can put into your C# project file (`.csproj`). Using such options, you can configure the generator behavior, the default field modifier, namespace and path filters. The generator supports the following options:
- `AvaloniaNameGeneratorBehavior`
Possible values: `OnlyProperties`, `InitializeComponent`
Default value: `InitializeComponent`
Determines if the generator should generate get-only properties, or the `InitializeComponent` method.
- `AvaloniaNameGeneratorDefaultFieldModifier`
Possible values: `internal`, `public`, `private`, `protected`
Default value: `internal`
The default field modifier that should be used when there is no `x:FieldModifier` directive specified.
- `AvaloniaNameGeneratorFilterByPath`
Posssible format: `glob_pattern`, `glob_pattern;glob_pattern`
Default value: `*`
The generator will process only XAML files with paths matching the specified glob pattern(s).
Example: `*/Views/*View.xaml`, `*View.axaml;*Control.axaml`
- `AvaloniaNameGeneratorFilterByNamespace`
Posssible format: `glob_pattern`, `glob_pattern;glob_pattern`
Default value: `*`
The generator will process only XAML files with base classes' namespaces matching the specified glob pattern(s).
Example: `MyApp.Presentation.*`, `MyApp.Presentation.Views;MyApp.Presentation.Controls`
- `AvaloniaNameGeneratorViewFileNamingStrategy`
Possible values: `ClassName`, `NamespaceAndClassName`
Default value: `NamespaceAndClassName`
Determines how the automatically generated view files should be [named](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/92).
The default values are given by:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AvaloniaNameGeneratorBehavior>InitializeComponent</AvaloniaNameGeneratorBehavior>
<AvaloniaNameGeneratorDefaultFieldModifier>internal</AvaloniaNameGeneratorDefaultFieldModifier>
<AvaloniaNameGeneratorFilterByPath>*</AvaloniaNameGeneratorFilterByPath>
<AvaloniaNameGeneratorFilterByNamespace>*</AvaloniaNameGeneratorFilterByNamespace>
<AvaloniaNameGeneratorViewFileNamingStrategy>NamespaceAndClassName</AvaloniaNameGeneratorViewFileNamingStrategy>
</PropertyGroup>
<!-- ... -->
</Project>
```
![](https://user-images.githubusercontent.com/6759207/107812261-7ddfea00-6d80-11eb-9c7e-67bf95d0f0d4.gif)
### What do the generated sources look like?
For [`SignUpView`](https://github.com/avaloniaui/Avalonia.NameGenerator/blob/main/src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml), we get the following generated output when the source generator is in the `InitializeComponent` mode:
```cs
// <auto-generated />
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox;
public global::Avalonia.Controls.TextBlock UserNameValidation;
private global::Avalonia.Controls.TextBox PasswordTextBox;
internal global::Avalonia.Controls.TextBlock PasswordValidation;
internal global::Avalonia.Controls.ListBox AwesomeListView;
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox;
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation;
internal global::Avalonia.Controls.Button SignUpButton;
internal global::Avalonia.Controls.TextBlock CompoundValidation;
public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
// This will be added only if you install Avalonia.Diagnostics.
#if DEBUG
if (attachDevTools)
{
this.AttachDevTools();
}
#endif
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox");
UserNameValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation");
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
PasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation");
AwesomeListView = this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("AwesomeListView");
ConfirmPasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox");
ConfirmPasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation");
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
CompoundValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation");
}
}
}
```
If you enable the `OnlyProperties` source generator mode, you get:
```cs
// <auto-generated />
using Avalonia.Controls;
namespace Avalonia.NameGenerator.Sandbox.Views
{
partial class SignUpView
{
internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox");
public global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation");
private global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox");
internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation");
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox");
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation");
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton");
internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation");
}
}
```

6
tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs

@ -16,9 +16,11 @@ namespace Avalonia.Base.UnitTests.Media
var typeface = new Typeface(fontFamily);
var glyphTypeface = FontManager.Current.GetOrAddGlyphTypeface(typeface);
Assert.True(FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface));
Assert.Same(glyphTypeface, FontManager.Current.GetOrAddGlyphTypeface(typeface));
FontManager.Current.TryGetGlyphTypeface(typeface, out var other);
Assert.Same(glyphTypeface, other);
}
}

16
tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs

@ -46,9 +46,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Single_FontAsset()
{
var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
var fontAssets = FontFamilyLoader.LoadFontAssets(source);
Assert.Single(fontAssets);
}
@ -57,9 +56,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Single_FontAsset_Avares_Without_BaseUri()
{
var source = new Uri(AssetYourFontAvares);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
var fontAssets = FontFamilyLoader.LoadFontAssets(source);
Assert.Single(fontAssets);
}
@ -69,9 +67,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
{
var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute);
var baseUri = new Uri(AssetLocationAvares);
var key = new FontFamilyKey(source, baseUri);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
var fontAssets = FontFamilyLoader.LoadFontAssets(new Uri(baseUri, source));
Assert.Single(fontAssets);
}
@ -80,9 +77,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Matching_Assets()
{
var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray();
var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray();
foreach (var fontAsset in fontAssets)
{
@ -99,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontFamily = new FontFamily("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono");
var source = new Uri("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono", UriKind.RelativeOrAbsolute);
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key).ToArray();
var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray();
Assert.NotEmpty(fontAssets);

25
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -100,6 +100,31 @@ namespace Avalonia.Controls.UnitTests
Assert.All(items, x => Assert.Same(theme, x.ItemContainerTheme));
}
[Fact]
public void Finds_Correct_DataTemplate_When_Application_DataTemplate_Is_Present()
{
// #10398
using var app = UnitTestApplication.Start();
Avalonia.Application.Current.DataTemplates.Add(new FuncDataTemplate<object>((x, _) => new Canvas()));
AvaloniaLocator.CurrentMutable.Bind<IGlobalDataTemplates>().ToConstant(Avalonia.Application.Current);
var target = new TreeView
{
Template = CreateTreeViewTemplate(),
Items = CreateTestTreeData(),
};
var root = new TestRoot(target);
CreateNodeDataTemplate(target);
ApplyTemplates(target);
Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0));
Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1));
Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2));
}
[Fact]
public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers()
{

61
tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs

@ -1,4 +1,5 @@
using Avalonia.Direct2D1.Media;
using System;
using Avalonia.Direct2D1.Media;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
@ -16,18 +17,10 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, Arial")));
var font = glyphTypeface.DWFont;
Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
var glyphTypeface =
new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface;
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
Assert.Equal("Arial", glyphTypeface.FamilyName);
}
}
@ -38,18 +31,13 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold));
var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
var font = glyphTypeface.DWFont;
Assert.Equal("Arial", glyphTypeface.FamilyName);
Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(FontWeight.Bold, glyphTypeface.Weight);
Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
Assert.Equal(FontStyle.Normal, glyphTypeface.Style);
}
}
@ -60,20 +48,11 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface;
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("Unknown")));
var defaultName = FontManager.Current.DefaultFontFamilyName;
var font = glyphTypeface.DWFont;
var defaultName = fontManager.GetDefaultFontFamilyName();
Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
Assert.Equal(defaultName, glyphTypeface.FamilyName);
}
}
@ -86,12 +65,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri));
var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface;
var font = glyphTypeface.DWFont;
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
@ -102,14 +78,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
var font = glyphTypeface.DWFont;
var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface;
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
}

26
tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Avalonia.Generators.Tests</RootNamespace>
<IsTestProject>true</IsTestProject>
</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.ReactiveUI\Avalonia.ReactiveUI.csproj" />
<ProjectReference Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.csproj" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Views\*.xml" />
<EmbeddedResource Include="OnlyProperties\GeneratedCode\*.txt" />
<EmbeddedResource Include="InitializeComponent\GeneratedInitializeComponent\*.txt" />
<EmbeddedResource Include="InitializeComponent\GeneratedDevTools\*.txt" />
</ItemGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\SharedVersion.props" />
</Project>

31
tests/Avalonia.Generators.Tests/GlobPatternTests.cs

@ -0,0 +1,31 @@
using Avalonia.Generators.Common;
using Xunit;
namespace Avalonia.Generators.Tests;
public class GlobPatternTests
{
[Theory]
[InlineData("*", "anything", true)]
[InlineData("", "anything", false)]
[InlineData("Views/*", "Views/SignUpView.xaml", true)]
[InlineData("Views/*", "Extensions/SignUpView.xaml", false)]
[InlineData("*SignUpView*", "Extensions/SignUpView.xaml", true)]
[InlineData("*SignUpView.paml", "Extensions/SignUpView.xaml", false)]
[InlineData("*.xaml", "Extensions/SignUpView.xaml", true)]
public void Should_Match_Glob_Expressions(string pattern, string value, bool matches)
{
Assert.Equal(matches, new GlobPattern(pattern).Matches(value));
}
[Theory]
[InlineData("Views/SignUpView.xaml", true, new[] { "*.xaml", "Extensions/*" })]
[InlineData("Extensions/SignUpView.paml", true, new[] { "*.xaml", "Extensions/*" })]
[InlineData("Extensions/SignUpView.paml", false, new[] { "*.xaml", "Views/*" })]
[InlineData("anything", true, new[] { "*", "*" })]
[InlineData("anything", false, new[] { "", "" })]
public void Should_Match_Glob_Pattern_Groups(string value, bool matches, string[] patterns)
{
Assert.Equal(matches, new GlobPatternGroup(patterns).Matches(value));
}
}

28
tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedProps.txt

@ -0,0 +1,28 @@
// <auto-generated />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sample.App
{
partial class SampleView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox;
/// <summary>
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
/// </summary>
/// <param name="loadXaml">Should the XAML be loaded into the component.</param>
public void InitializeComponent(bool loadXaml = true)
{
if (loadXaml)
{
AvaloniaXamlLoader.Load(this);
}
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox");
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save