diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
index 5bbc3d6915..875161d336 100644
--- a/.nuke/build.schema.json
+++ b/.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"
]
}
diff --git a/Avalonia.sln b/Avalonia.sln
index e66b73de0e..b21df07628 100644
--- a/Avalonia.sln
+++ b/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}
diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props
index 4929578b60..a66bff4999 100644
--- a/build/SourceGenerators.props
+++ b/build/SourceGenerators.props
@@ -1,5 +1,10 @@
-
+
+ true
+ false
+
+
+
+
+
+
+
+
diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm
index 4ae6ad5a00..bcfdc23053 100644
--- a/native/Avalonia.Native/src/OSX/AvnView.mm
+++ b/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);
}
}
diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
index 59102e15a6..b579920c6b 100644
--- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
+++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
@@ -4,6 +4,7 @@
//
#import
+#import
#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 {
diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm
index cf1ee6943d..840f2c9e88 100644
--- a/native/Avalonia.Native/src/OSX/WindowImpl.mm
+++ b/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);
}
}
diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs
index 46f267ae17..40232947d9 100644
--- a/nukebuild/Build.cs
+++ b/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);
diff --git a/nukebuild/numerge.config b/nukebuild/numerge.config
index d1c0408241..09f22ec527 100644
--- a/nukebuild/numerge.config
+++ b/nukebuild/numerge.config
@@ -11,6 +11,11 @@
"Id": "Avalonia.Build.Tasks",
"IgnoreMissingFrameworkBinaries": true,
"DoNotMergeDependencies": true
+ },
+ {
+ "Id": "Avalonia.Generators",
+ "IgnoreMissingFrameworkBinaries": true,
+ "DoNotMergeDependencies": true
}
]
}
diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj
index 4d0ed866a3..1d210172f0 100644
--- a/packages/Avalonia/Avalonia.csproj
+++ b/packages/Avalonia/Avalonia.csproj
@@ -6,11 +6,15 @@
-
+
all
true
TargetFramework=netstandard2.0
+
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index e55f003133..85c159467b 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/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()
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index 0e84b3d182..5125b42426 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -2,7 +2,8 @@
netstandard2.0;net6.0
true
- enable
+ enable
+ true
@@ -35,14 +36,5 @@
-
-
-
-
-
-
-
-
-
-
+
diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
index 6d624c9a07..6d759597b5 100644
--- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
@@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Get("fontComboBox");
- fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
+ fontComboBox.Items = FontManager.Current.SystemFonts;
fontComboBox.SelectedIndex = 0;
}
}
diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs b/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
index 7db6d9d334..8944151385 100644
--- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
+++ b/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("ButtonFlyoutXamlText");
diff --git a/samples/ControlCatalog/Pages/LabelsPage.axaml.cs b/samples/ControlCatalog/Pages/LabelsPage.axaml.cs
index f05e5fd033..f3a7647f8c 100644
--- a/samples/ControlCatalog/Pages/LabelsPage.axaml.cs
+++ b/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()
{
diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
index f9d0328d9a..a710cd7e5c 100644
--- a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
+++ b/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);
- }
}
}
diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
index 11d0a5152e..aec13a18e3 100644
--- a/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
+++ b/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();
}
}
}
diff --git a/samples/ControlCatalog/Pages/ThemePage.axaml.cs b/samples/ControlCatalog/Pages/ThemePage.axaml.cs
index f0ae1a722d..5a0c4cba43 100644
--- a/samples/ControlCatalog/Pages/ThemePage.axaml.cs
+++ b/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("Selector")!;
- var themeVariantScope = this.FindControl("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;
}
};
}
diff --git a/samples/Generators.Sandbox/App.xaml b/samples/Generators.Sandbox/App.xaml
new file mode 100644
index 0000000000..8064eac3f5
--- /dev/null
+++ b/samples/Generators.Sandbox/App.xaml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/Generators.Sandbox/App.xaml.cs b/samples/Generators.Sandbox/App.xaml.cs
new file mode 100644
index 0000000000..6118b3f177
--- /dev/null
+++ b/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();
+ }
+}
\ No newline at end of file
diff --git a/samples/Generators.Sandbox/Controls/CustomTextBox.cs b/samples/Generators.Sandbox/Controls/CustomTextBox.cs
new file mode 100644
index 0000000000..68ee925986
--- /dev/null
+++ b/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);
+}
\ No newline at end of file
diff --git a/samples/Generators.Sandbox/Controls/SignUpView.xaml b/samples/Generators.Sandbox/Controls/SignUpView.xaml
new file mode 100644
index 0000000000..c126f36f53
--- /dev/null
+++ b/samples/Generators.Sandbox/Controls/SignUpView.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Generators.Sandbox/Controls/SignUpView.xaml.cs b/samples/Generators.Sandbox/Controls/SignUpView.xaml.cs
new file mode 100644
index 0000000000..c4cd1cdc1a
--- /dev/null
+++ b/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;
+
+///
+/// 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/
+///
+public partial class SignUpView : ReactiveUserControl
+{
+ 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.";
+ });
+ }
+}
diff --git a/samples/Generators.Sandbox/Generators.Sandbox.csproj b/samples/Generators.Sandbox/Generators.Sandbox.csproj
new file mode 100644
index 0000000000..885e71af4e
--- /dev/null
+++ b/samples/Generators.Sandbox/Generators.Sandbox.csproj
@@ -0,0 +1,28 @@
+
+
+ Exe
+ net6.0
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Generators.Sandbox/Program.cs b/samples/Generators.Sandbox/Program.cs
new file mode 100644
index 0000000000..7e533965df
--- /dev/null
+++ b/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()
+ .UseReactiveUI()
+ .UsePlatformDetect()
+ .LogToTrace();
+}
diff --git a/samples/Generators.Sandbox/ViewModels/SignUpViewModel.cs b/samples/Generators.Sandbox/ViewModels/SignUpViewModel.cs
new file mode 100644
index 0000000000..06b6122088
--- /dev/null
+++ b/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 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);
+ }
+}
\ No newline at end of file
diff --git a/samples/Generators.Sandbox/Views/SignUpView.xaml b/samples/Generators.Sandbox/Views/SignUpView.xaml
new file mode 100644
index 0000000000..970b3a9710
--- /dev/null
+++ b/samples/Generators.Sandbox/Views/SignUpView.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/samples/Generators.Sandbox/Views/SignUpView.xaml.cs b/samples/Generators.Sandbox/Views/SignUpView.xaml.cs
new file mode 100644
index 0000000000..1f495b7dc6
--- /dev/null
+++ b/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;
+
+///
+/// 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/
+///
+public partial class SignUpView : ReactiveWindow
+{
+ 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);
+ });
+ }
+}
\ No newline at end of file
diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml
index 5162eeee92..bd6910dd4d 100644
--- a/samples/IntegrationTestApp/ShowWindowTest.axaml
+++ b/samples/IntegrationTestApp/ShowWindowTest.axaml
@@ -1,41 +1,48 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Normal
- Minimized
- Maximized
- FullScreen
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Normal
+ Minimized
+ Maximized
+ FullScreen
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs
index 1a267ea20b..f0be34fdaa 100644
--- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs
+++ b/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 MeasuredWithProperty = AvaloniaProperty.Register(
+ nameof(MeasuredWith));
+
+ public Size MeasuredWith
+ {
+ get => GetValue(MeasuredWithProperty);
+ set => SetValue(MeasuredWithProperty, value);
+ }
+ }
+
public class ShowWindowTest : Window
{
private readonly DispatcherTimer? _timer;
diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs
index 2dabb29e76..595a2f3474 100644
--- a/src/Avalonia.Base/Media/FontManager.cs
+++ b/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
///
public sealed class FontManager
{
- private readonly ConcurrentDictionary _glyphTypefaceCache =
- new ConcurrentDictionary();
- private readonly FontFamily _defaultFontFamily;
+ internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts");
+
+ public const string FontCollectionScheme = "fonts";
+
+ private readonly ConcurrentDictionary _fontCollections = new ConcurrentDictionary();
private readonly IReadOnlyList? _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));
}
+ ///
+ /// Get the current font manager instance.
+ ///
public static FontManager Current
{
get
@@ -57,11 +64,6 @@ namespace Avalonia.Media
}
}
- ///
- ///
- ///
- public IFontManagerImpl PlatformImpl { get; }
-
///
/// Gets the system's default font family's name.
///
@@ -71,41 +73,109 @@ namespace Avalonia.Media
}
///
- /// Get all installed font family names.
+ /// Get all system fonts.
///
- /// If true the font collection is updated.
- public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
- PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
+ public IFontCollection SystemFonts => _fontCollections[SystemFontsKey];
+
+ internal IFontManagerImpl PlatformImpl { get; }
///
- /// Returns a new , or an existing one if a matching exists.
+ /// Tries to get a glyph typeface for specified typeface.
///
/// The typeface.
+ /// The created glyphTypeface
///
- /// The .
+ /// True, if the could create the glyph typeface, False otherwise.
///
- 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);
+ }
+
+ ///
+ /// Add a font collection to the manager.
+ ///
+ /// The font collection.
+ ///
+ /// If a font collection's key is already present the collection is replaced.
+ 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);
+ }
+
+ ///
+ /// Removes the font collection that corresponds to specified key.
+ ///
+ /// The font collection's key.
+ public void RemoveFontCollection(Uri key)
+ {
+ if (_fontCollections.TryRemove(key, out var fontCollection))
+ {
+ fontCollection.Dispose();
}
}
@@ -123,18 +193,16 @@ namespace Avalonia.Media
/// True, if the could match the character to specified parameters, False otherwise.
///
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;
}
}
diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
new file mode 100644
index 0000000000..f2fb490592
--- /dev/null
+++ b/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> _glyphTypefaceCache = new();
+
+ private readonly List _fontFamilies = new List(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();
+
+ 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();
+
+ 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 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 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 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 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;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs
new file mode 100644
index 0000000000..0d0dc3016e
--- /dev/null
+++ b/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);
+}
diff --git a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
index 365fb6e412..37992c895e 100644
--- a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
+++ b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
@@ -11,22 +11,30 @@ namespace Avalonia.Media.Fonts
///
/// Loads all font assets that belong to the specified
///
- ///
+ ///
///
- public static IEnumerable LoadFontAssets(FontFamilyKey fontFamilyKey) =>
- IsFontTtfOrOtf(fontFamilyKey.Source) ?
- GetFontAssetsByExpression(fontFamilyKey) :
- GetFontAssetsBySource(fontFamilyKey);
+ public static IEnumerable LoadFontAssets(Uri source)
+ {
+ if (source.IsAvares() || source.IsAbsoluteResm())
+ {
+ return IsFontTtfOrOtf(source) ?
+ GetFontAssetsByExpression(source) :
+ GetFontAssetsBySource(source);
+ }
+
+ return Enumerable.Empty();
+ }
+
///
/// Searches for font assets at a given location and returns a quantity of found assets
///
- ///
+ ///
///
- private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey)
+ private static IEnumerable GetFontAssetsBySource(Uri source)
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService();
- 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.
/// File names can target multiple files with * wildcard. For example "FontFile*.ttf"
///
- ///
+ ///
///
- private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
+ private static IEnumerable 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();
- 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;
}
diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs
new file mode 100644
index 0000000000..814230bcf3
--- /dev/null
+++ b/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, IDisposable
+ {
+ ///
+ /// Get the font collection's key.
+ ///
+ Uri Key { get; }
+
+ ///
+ /// Initializes the font collection.
+ ///
+ /// The font manager the collection is registered with.
+ void Initialize(IFontManagerImpl fontManager);
+
+ ///
+ /// Try to get a glyph typeface for given parameters.
+ ///
+ /// The family name.
+ /// The font style.
+ /// The font weight.
+ /// The font stretch.
+ /// The glyph typeface.
+ /// Returns true if a glyph typface can be found; otherwise, false
+ bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+ FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
+ }
+}
diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
new file mode 100644
index 0000000000..fd332c6ebe
--- /dev/null
+++ b/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> _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();
+
+ 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 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);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Media/IGlyphTypeface.cs b/src/Avalonia.Base/Media/IGlyphTypeface.cs
index 9e1e52cb73..09740aac81 100644
--- a/src/Avalonia.Base/Media/IGlyphTypeface.cs
+++ b/src/Avalonia.Base/Media/IGlyphTypeface.cs
@@ -6,6 +6,26 @@ namespace Avalonia.Media
[Unstable]
public interface IGlyphTypeface : IDisposable
{
+ ///
+ /// Gets the family name for the object.
+ ///
+ string FamilyName { get; }
+
+ ///
+ /// Gets the designed weight of the font represented by the object.
+ ///
+ FontWeight Weight { get; }
+
+ ///
+ /// Gets the style for the object.
+ ///
+ FontStyle Style { get; }
+
+ ///
+ /// Gets the value for the object.
+ ///
+ FontStretch Stretch { get; }
+
///
/// Gets the number of glyphs held by this glyph typeface.
///
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
index b4734d702b..253c7075fa 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
+++ b/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
diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs
index 1e744c30c8..e2729c9158 100644
--- a/src/Avalonia.Base/Media/Typeface.cs
+++ b/src/Avalonia.Base/Media/Typeface.cs
@@ -80,7 +80,18 @@ namespace Avalonia.Media
///
/// The glyph typeface.
///
- 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)
{
diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs
index cd6e64abaf..116f7cd6e2 100644
--- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs
+++ b/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.
/// If true the font collection is updated.
///
- IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false);
+ string[] GetInstalledFontFamilyNames(bool checkForUpdates = false);
///
/// 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);
///
- /// Creates a glyph typeface.
+ /// Tries to get a glyph typeface for specified parameters.
///
- /// The typeface.
- /// 0
- /// The created glyph typeface. Can be Null if it was not possible to create a glyph typeface.
+ /// The family name.
+ /// The font style.
+ /// The font weiht.
+ /// The font stretch.
+ /// The created glyphTypeface
+ ///
+ /// True, if the could create the glyph typeface, False otherwise.
+ ///
+ bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+ FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
+
+ ///
+ /// Tries to create a glyph typeface from specified stream.
+ ///
+ /// A stream that holds the font's data.
+ /// The created glyphTypeface
+ ///
+ /// True, if the could create the glyph typeface, False otherwise.
///
- IGlyphTypeface CreateGlyphTypeface(Typeface typeface);
+ bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
}
}
diff --git a/src/Avalonia.Base/Utilities/UriExtensions.cs b/src/Avalonia.Base/Utilities/UriExtensions.cs
index c706f72a63..1f9c694eab 100644
--- a/src/Avalonia.Base/Utilities/UriExtensions.cs
+++ b/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)
diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs
index cf79fcd1a8..64bf92b7cd 100644
--- a/src/Avalonia.Controls/AppBuilder.cs
+++ b/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;
}
+ ///
+ /// Registers an action that is executed with the current font manager.
+ ///
+ /// The action.
+ /// An instance.
+ public AppBuilder ConfigureFonts(Action action)
+ {
+ return AfterSetup(appBuilder =>
+ {
+ action?.Invoke(FontManager.Current);
+ });
+ }
+
///
/// Sets up the platform-specific services for the .
///
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 9483f98881..bebf4a38f6 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/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();
}
}
diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
index 71ae7a5bf6..55d2ec7506 100644
--- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
+++ b/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
///
public class HeaderedItemsControl : ItemsControl, IContentPresenterHost
{
+ private IDisposable? _itemsBinding;
+ private bool _prepareItemContainerOnAttach;
+
///
/// Defines the property.
///
@@ -60,6 +65,17 @@ namespace Avalonia.Controls.Primitives
///
IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren;
+ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToLogicalTree(e);
+
+ if (_prepareItemContainerOnAttach)
+ {
+ PrepareItemContainer();
+ _prepareItemContainerOnAttach = false;
+ }
+ }
+
///
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)
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs
new file mode 100644
index 0000000000..b7579ed31b
--- /dev/null
+++ b/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
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty BrushProperty =
+ AvaloniaProperty.RegisterDirect(
+ 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));
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs
new file mode 100644
index 0000000000..7870febd0a
--- /dev/null
+++ b/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);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty CommittedTextProperty =
+ AvaloniaProperty.RegisterDirect(
+ 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);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
index 0e412a2fa5..2412ea5325 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
+++ b/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()
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
index 895ff41f7b..b7ee1459f7 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
+++ b/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 { }
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
index a7faf35769..aa2682e376 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
+++ b/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; }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs
new file mode 100644
index 0000000000..9425989096
--- /dev/null
+++ b/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 GetObservable(
+ this TOwner vm,
+ Expression> property,
+ bool fireImmediately = true)
+ where TOwner : INotifyPropertyChanged
+ {
+ return Observable.Create(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(this Expression> property)
+ {
+ if (property.Body is UnaryExpression unaryExpression)
+ {
+ return (PropertyInfo)((MemberExpression)unaryExpression.Operand).Member;
+ }
+
+ var memExpr = (MemberExpression)property.Body;
+
+ return (PropertyInfo)memExpr.Member;
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
index 2a69798c6c..97f195c91b 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
@@ -60,7 +60,11 @@
DoubleTapped="PropertiesGrid_OnDoubleTapped">
-
+
+
+
+
+
("console");
_consoleSplitter = this.GetControl("consoleSplitter");
_rootGrid = this.GetControl("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)
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
index 748c2cc313..486df860c3 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
@@ -15,6 +15,7 @@
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs
new file mode 100644
index 0000000000..6e7729a350
--- /dev/null
+++ b/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(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(AvaloniaProperty valueProperty,
+ IValueConverter? converter = null,
+ Action? 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(ToggleButton.IsCheckedProperty);
+
+ //TODO: Infinity, NaN not working with NumericUpDown
+ if (propertyType.IsPrimitive && propertyType != typeof(float) && propertyType != typeof(double))
+ return CreateControl(
+ 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(propertyType))
+ return CreateControl(BrushEditor.BrushProperty);
+
+ var isImage = ImplementsInterface(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(
+ SelectingItemsControl.SelectedItemProperty, init: c =>
+ {
+ c.Items = Enum.GetValues(propertyType);
+ });
+
+ var tb = CreateControl(
+ 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;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs b/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs
new file mode 100644
index 0000000000..842629c923
--- /dev/null
+++ b/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());
+ });
+ }
+ }
+}
diff --git a/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj b/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj
index c81a13558c..c18c07d347 100644
--- a/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj
+++ b/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/Avalonia.Fonts.Inter/InterFontCollection.cs b/src/Avalonia.Fonts.Inter/InterFontCollection.cs
new file mode 100644
index 0000000000..0ed1779a03
--- /dev/null
+++ b/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))
+ {
+ }
+ }
+}
diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
index 46e3515d11..ee4cd5af98 100644
--- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs
+++ b/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 GetInstalledFontFamilyNames(bool checkForUpdates = false)
+ public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface)
{
- return new List { "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,
diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml
index 82e48851b5..c19a4f5c09 100644
--- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml
@@ -3,7 +3,7 @@
xmlns:sys="using:System"
xmlns:converters="using:Avalonia.Controls.Converters">
- avares://Avalonia.Fonts.Inter/Assets#Inter, $Default
+ fonts:Inter#Inter, $Default
14
diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml
index 0a06927034..38b122d8b2 100644
--- a/src/Avalonia.Themes.Simple/Accents/Base.xaml
+++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml
@@ -76,7 +76,7 @@
- avares://Avalonia.Fonts.Inter/Assets#Inter, $Default
+ fonts://Inter#Inter, $Default
#CC119EDA
#99119EDA
#66119EDA
diff --git a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
index 8428e3aae7..0c7095f2f5 100644
--- a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
@@ -92,6 +92,7 @@
+
diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
index 90a2f9169b..29e5687423 100644
--- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs
+++ b/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 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;
}
}
}
diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
index 3093455bec..43e10e3e96 100644
--- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
+++ b/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;
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
deleted file mode 100644
index 9ee17a09d6..0000000000
--- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
+++ /dev/null
@@ -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 _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;
- }
- }
-}
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
deleted file mode 100644
index d064f49ae4..0000000000
--- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
+++ /dev/null
@@ -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 s_cachedCollections;
-
- static SKTypefaceCollectionCache()
- {
- s_cachedCollections = new ConcurrentDictionary();
- }
-
- ///
- /// Gets the or add typeface collection.
- ///
- /// The font family.
- ///
- public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily)
- {
- return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection);
- }
-
- ///
- /// Creates the custom font collection.
- ///
- /// The font family.
- ///
- 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();
-
- 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;
- }
- }
-}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs
index 4663a6561f..b60962a091 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs
+++ b/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
///
/// The factory.
///
- public DWriteResourceFontLoader(Factory factory, IEnumerable fontAssets)
+ public DWriteResourceFontLoader(Factory factory, Stream[] fontAssets)
{
var factory1 = factory;
- var assetLoader = AvaloniaLocator.Current.GetRequiredService();
-
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;
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
index 792bf2d0be..ad2ede3a91 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
+++ b/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();
+
+ var fontAssets = assets.Select(x => assetLoader.Open(x)).ToArray();
+
+ var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, fontAssets);
return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
index b98ed3ffe6..ec2f6385da 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
+++ b/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 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;
}
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
index e4988322e7..01add0f0cb 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
+++ b/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();
@@ -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; }
+
///
public ushort GetGlyph(uint codepoint)
{
diff --git a/src/tools/Avalonia.Generators/Avalonia.Generators.csproj b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj
new file mode 100644
index 0000000000..c6e32a3a4f
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj
@@ -0,0 +1,32 @@
+
+
+ netstandard2.0
+ false
+ Avalonia.Generators
+ $(DefineConstants);XAMLX_INTERNAL
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tools/Avalonia.Generators/Avalonia.Generators.props b/src/tools/Avalonia.Generators/Avalonia.Generators.props
new file mode 100644
index 0000000000..08cbeff1ba
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Avalonia.Generators.props
@@ -0,0 +1,22 @@
+
+
+ true
+ InitializeComponent
+ internal
+ *
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs b/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs
new file mode 100644
index 0000000000..4b426172f8
--- /dev/null
+++ b/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 names);
+}
diff --git a/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs b/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs
new file mode 100644
index 0000000000..04dbf9cbb9
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs
@@ -0,0 +1,6 @@
+namespace Avalonia.Generators.Common.Domain;
+
+internal interface IGlobPattern
+{
+ bool Matches(string str);
+}
diff --git a/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs b/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs
new file mode 100644
index 0000000000..cb5488d8a3
--- /dev/null
+++ b/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 ResolveNames(XamlDocument xaml);
+}
+
+internal record ResolvedName(string TypeName, string Name, string FieldModifier);
diff --git a/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs b/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs
new file mode 100644
index 0000000000..c3c219e3f0
--- /dev/null
+++ b/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);
diff --git a/src/tools/Avalonia.Generators/Common/GlobPattern.cs b/src/tools/Avalonia.Generators/Common/GlobPattern.cs
new file mode 100644
index 0000000000..484e17d787
--- /dev/null
+++ b/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);
+}
diff --git a/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs b/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs
new file mode 100644
index 0000000000..1358ee7920
--- /dev/null
+++ b/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 patterns) =>
+ _patterns = patterns
+ .Select(pattern => new GlobPattern(pattern))
+ .ToArray();
+
+ public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str));
+}
diff --git a/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs b/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs
new file mode 100644
index 0000000000..04352298c8
--- /dev/null
+++ b/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");
+}
diff --git a/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs b/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs
new file mode 100644
index 0000000000..7ed19eb84c
--- /dev/null
+++ b/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 _items = new();
+ private readonly string _defaultFieldModifier;
+
+ public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal)
+ {
+ _defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant();
+ }
+
+ public IReadOnlyList 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()
+ .Where(dir => dir.Name == "FieldModifier" && dir.Namespace == XamlNamespaces.Xaml2006)
+ .Select(dir => dir.Values[0])
+ .OfType()
+ .Select(txt => txt.Text)
+ .FirstOrDefault();
+
+ return fieldModifierType?.ToLowerInvariant() switch
+ {
+ "private" => "private",
+ "public" => "public",
+ "protected" => "protected",
+ "internal" => "internal",
+ "notpublic" => "internal",
+ _ => _defaultFieldModifier
+ };
+ }
+}
diff --git a/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs b/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs
new file mode 100644
index 0000000000..5bbe0c060d
--- /dev/null
+++ b/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 _onTypeInvalid;
+ private readonly Action _onUnhandledError;
+
+ private ResolvedView _resolvedClass;
+ private XamlDocument _xaml;
+
+ public XamlXViewResolver(
+ RoslynTypeSystem typeSystem,
+ MiniCompiler compiler,
+ bool checkTypeValidity = false,
+ Action onTypeInvalid = null,
+ Action 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
+ {
+ {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() { }
+}
diff --git a/src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs b/src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs
new file mode 100644
index 0000000000..e7c60c79ad
--- /dev/null
+++ b/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;
+ }
+}
\ No newline at end of file
diff --git a/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs b/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs
new file mode 100644
index 0000000000..71f34d173c
--- /dev/null
+++ b/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