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 @@ + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + 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 +{ + public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; + + public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes) + { + var mappings = new XamlLanguageTypeMappings(typeSystem); + foreach (var additionalType in additionalTypes) + mappings.XmlnsAttributes.Add(typeSystem.GetType(additionalType)); + + var configuration = new TransformerConfiguration( + typeSystem, + typeSystem.Assemblies.First(), + mappings); + return new MiniCompiler(configuration); + } + + private MiniCompiler(TransformerConfiguration configuration) + : base(configuration, new XamlLanguageEmitMappings(), false) + { + Transformers.Add(new NameDirectiveTransformer()); + Transformers.Add(new DataTemplateTransformer()); + Transformers.Add(new KnownDirectivesTransformer()); + Transformers.Add(new XamlIntrinsicsTransformer()); + Transformers.Add(new XArgumentsTransformer()); + Transformers.Add(new TypeReferenceResolver()); + } + + protected override XamlEmitContext InitCodeGen( + IFileSource file, + Func> createSubType, + Func, + IXamlTypeBuilder> createDelegateType, + object codeGen, + XamlRuntimeContext context, + bool needContextLocal) => + throw new NotSupportedException(); +} \ No newline at end of file diff --git a/src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs b/src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs new file mode 100644 index 0000000000..2d4d3225b7 --- /dev/null +++ b/src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs @@ -0,0 +1,28 @@ +using XamlX; +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Generators.Compiler; + +internal class NameDirectiveTransformer : IXamlAstTransformer +{ + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is not XamlAstObjectNode objectNode) + return node; + + for (var index = 0; index < objectNode.Children.Count; index++) + { + var child = objectNode.Children[index]; + if (child is XamlAstXmlDirective directive && + directive.Namespace == XamlNamespaces.Xaml2006 && + directive.Name == "Name") + objectNode.Children[index] = new XamlAstXamlPropertyValueNode( + directive, + new XamlAstNamePropertyReference(directive, objectNode.Type, "Name", objectNode.Type), + directive.Values); + } + + return node; + } +} \ No newline at end of file diff --git a/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs b/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs new file mode 100644 index 0000000000..851bc3f29b --- /dev/null +++ b/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using XamlX.TypeSystem; + +namespace Avalonia.Generators.Compiler; + +internal class RoslynTypeSystem : IXamlTypeSystem +{ + private readonly List _assemblies = new(); + + public RoslynTypeSystem(CSharpCompilation compilation) + { + _assemblies.Add(new RoslynAssembly(compilation.Assembly)); + + var assemblySymbols = compilation + .References + .Select(compilation.GetAssemblyOrModuleSymbol) + .OfType() + .Select(assembly => new RoslynAssembly(assembly)) + .ToList(); + + _assemblies.AddRange(assemblySymbols); + } + + public IEnumerable Assemblies => _assemblies; + + public IXamlAssembly FindAssembly(string name) => + Assemblies + .FirstOrDefault(a => string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase)); + + public IXamlType FindType(string name) => + _assemblies + .Select(assembly => assembly.FindType(name)) + .FirstOrDefault(type => type != null); + + public IXamlType FindType(string name, string assembly) => + _assemblies + .Select(assemblyInstance => assemblyInstance.FindType(name)) + .FirstOrDefault(type => type != null); +} + +internal class RoslynAssembly : IXamlAssembly +{ + private readonly IAssemblySymbol _symbol; + + public RoslynAssembly(IAssemblySymbol symbol) => _symbol = symbol; + + public bool Equals(IXamlAssembly other) => + other is RoslynAssembly roslynAssembly && + SymbolEqualityComparer.Default.Equals(_symbol, roslynAssembly._symbol); + + public string Name => _symbol.Name; + + public IReadOnlyList CustomAttributes => + _symbol.GetAttributes() + .Select(data => new RoslynAttribute(data, this)) + .ToList(); + + public IXamlType FindType(string fullName) + { + var type = _symbol.GetTypeByMetadataName(fullName); + return type is null ? null : new RoslynType(type, this); + } +} + +internal class RoslynAttribute : IXamlCustomAttribute +{ + private readonly AttributeData _data; + private readonly RoslynAssembly _assembly; + + public RoslynAttribute(AttributeData data, RoslynAssembly assembly) + { + _data = data; + _assembly = assembly; + } + + public bool Equals(IXamlCustomAttribute other) => + other is RoslynAttribute attribute && + _data == attribute._data; + + public IXamlType Type => new RoslynType(_data.AttributeClass, _assembly); + + public List Parameters => + _data.ConstructorArguments + .Select(argument => argument.Value) + .ToList(); + + public Dictionary Properties => + _data.NamedArguments.ToDictionary( + pair => pair.Key, + pair => pair.Value.Value); +} + +internal class RoslynType : IXamlType +{ + private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | + SymbolDisplayGenericsOptions.IncludeTypeConstraints | + SymbolDisplayGenericsOptions.IncludeVariance); + + private readonly RoslynAssembly _assembly; + private readonly INamedTypeSymbol _symbol; + + public RoslynType(INamedTypeSymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlType other) => + other is RoslynType roslynType && + SymbolEqualityComparer.Default.Equals(_symbol, roslynType._symbol); + + public object Id => _symbol; + + public string Name => _symbol.Name; + + public string Namespace => _symbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat); + + public string FullName => $"{Namespace}.{Name}"; + + public IXamlAssembly Assembly => _assembly; + + public IReadOnlyList Properties => + _symbol.GetMembers() + .Where(member => member.Kind == SymbolKind.Property) + .OfType() + .Select(property => new RoslynProperty(property, _assembly)) + .ToList(); + + public IReadOnlyList Events { get; } = new List(); + + public IReadOnlyList Fields { get; } = new List(); + + public IReadOnlyList Methods { get; } = new List(); + + public IReadOnlyList Constructors => + _symbol.Constructors + .Select(method => new RoslynConstructor(method, _assembly)) + .ToList(); + + public IReadOnlyList CustomAttributes { get; } = new List(); + + public IReadOnlyList GenericArguments { get; private set; } = new List(); + + public bool IsAssignableFrom(IXamlType type) => type == this; + + public IXamlType MakeGenericType(IReadOnlyList typeArguments) + { + GenericArguments = typeArguments; + return this; + } + + public IXamlType GenericTypeDefinition => this; + + public bool IsArray => false; + + public IXamlType ArrayElementType { get; } = null; + + public IXamlType MakeArrayType(int dimensions) => null; + + public IXamlType BaseType => _symbol.BaseType == null ? null : new RoslynType(_symbol.BaseType, _assembly); + + public bool IsValueType { get; } = false; + + public bool IsEnum { get; } = false; + + public IReadOnlyList Interfaces => + _symbol.AllInterfaces + .Select(abstraction => new RoslynType(abstraction, _assembly)) + .ToList(); + + public bool IsInterface => _symbol.IsAbstract; + + public IXamlType GetEnumUnderlyingType() => null; + + public IReadOnlyList GenericParameters { get; } = new List(); +} + +internal class RoslynConstructor : IXamlConstructor +{ + private readonly IMethodSymbol _symbol; + private readonly RoslynAssembly _assembly; + + public RoslynConstructor(IMethodSymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlConstructor other) => + other is RoslynConstructor roslynConstructor && + SymbolEqualityComparer.Default.Equals(_symbol, roslynConstructor._symbol); + + public bool IsPublic => true; + + public bool IsStatic => false; + + public IReadOnlyList Parameters => + _symbol.Parameters + .Select(parameter => parameter.Type) + .OfType() + .Select(type => new RoslynType(type, _assembly)) + .ToList(); +} + +internal class RoslynProperty : IXamlProperty +{ + private readonly IPropertySymbol _symbol; + private readonly RoslynAssembly _assembly; + + public RoslynProperty(IPropertySymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlProperty other) => + other is RoslynProperty roslynProperty && + SymbolEqualityComparer.Default.Equals(_symbol, roslynProperty._symbol); + + public string Name => _symbol.Name; + + public IXamlType PropertyType => + _symbol.Type is INamedTypeSymbol namedTypeSymbol + ? new RoslynType(namedTypeSymbol, _assembly) + : null; + + public IXamlMethod Getter => _symbol.GetMethod == null ? null : new RoslynMethod(_symbol.GetMethod, _assembly); + + public IXamlMethod Setter => _symbol.SetMethod == null ? null : new RoslynMethod(_symbol.SetMethod, _assembly); + + public IReadOnlyList CustomAttributes { get; } = new List(); + + public IReadOnlyList IndexerParameters { get; } = new List(); +} + +internal class RoslynMethod : IXamlMethod +{ + private readonly IMethodSymbol _symbol; + private readonly RoslynAssembly _assembly; + + public RoslynMethod(IMethodSymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlMethod other) => + other is RoslynMethod roslynMethod && + SymbolEqualityComparer.Default.Equals(roslynMethod._symbol, _symbol); + + public string Name => _symbol.Name; + + public bool IsPublic => true; + + public bool IsStatic => false; + + public IXamlType ReturnType => new RoslynType((INamedTypeSymbol) _symbol.ReturnType, _assembly); + + public IReadOnlyList Parameters => + _symbol.Parameters.Select(parameter => parameter.Type) + .OfType() + .Select(type => new RoslynType(type, _assembly)) + .ToList(); + + public IXamlType DeclaringType => new RoslynType((INamedTypeSymbol)_symbol.ReceiverType, _assembly); + + public IXamlMethod MakeGenericMethod(IReadOnlyList typeArguments) => null; + + public IReadOnlyList CustomAttributes { get; } = new List(); +} diff --git a/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs b/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs new file mode 100644 index 0000000000..d057e8732e --- /dev/null +++ b/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.CodeAnalysis; + +namespace Avalonia.Generators; + +internal static class GeneratorContextExtensions +{ + private const string UnhandledErrorDescriptorId = "AXN0002"; + private const string InvalidTypeDescriptorId = "AXN0001"; + + public static string GetMsBuildProperty( + this GeneratorExecutionContext context, + string name, + string defaultValue = "") + { + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value); + return value ?? defaultValue; + } + + public static void ReportNameGeneratorUnhandledError(this GeneratorExecutionContext context, Exception error) => + context.Report(UnhandledErrorDescriptorId, + "Unhandled exception occured while generating typed Name references. " + + "Please file an issue: https://github.com/avaloniaui/Avalonia.Generators", + error.ToString()); + + public static void ReportNameGeneratorInvalidType(this GeneratorExecutionContext context, string typeName) => + context.Report(InvalidTypeDescriptorId, + $"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " + + $"The type '{typeName}' does not exist in the assembly."); + + private static void Report(this GeneratorExecutionContext context, string id, string title, string message = null) => + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor(id, title, message ?? title, "Usage", DiagnosticSeverity.Error, true), + Location.None)); +} diff --git a/src/tools/Avalonia.Generators/GeneratorOptions.cs b/src/tools/Avalonia.Generators/GeneratorOptions.cs new file mode 100644 index 0000000000..bfa91f691c --- /dev/null +++ b/src/tools/Avalonia.Generators/GeneratorOptions.cs @@ -0,0 +1,71 @@ +using System; +using Avalonia.Generators.Common.Domain; +using Avalonia.Generators.NameGenerator; +using Microsoft.CodeAnalysis; + +namespace Avalonia.Generators; + +// When update these enum values, don't forget to update Avalonia.Generators.props. +internal enum BuildProperties +{ + AvaloniaNameGeneratorIsEnabled = 0, + AvaloniaNameGeneratorBehavior = 1, + AvaloniaNameGeneratorDefaultFieldModifier = 2, + AvaloniaNameGeneratorFilterByPath = 3, + AvaloniaNameGeneratorFilterByNamespace = 4, + AvaloniaNameGeneratorViewFileNamingStrategy = 5, + + // TODO add other generators properties here. +} + +internal class GeneratorOptions +{ + private readonly GeneratorExecutionContext _context; + + public GeneratorOptions(GeneratorExecutionContext context) => _context = context; + + public bool AvaloniaNameGeneratorIsEnabled => GetBoolProperty( + BuildProperties.AvaloniaNameGeneratorIsEnabled, + true); + + public Behavior AvaloniaNameGeneratorBehavior => GetEnumProperty( + BuildProperties.AvaloniaNameGeneratorBehavior, + Behavior.InitializeComponent); + + public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier => GetEnumProperty( + BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier, + NamedFieldModifier.Internal); + + public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy => GetEnumProperty( + BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy, + ViewFileNamingStrategy.NamespaceAndClassName); + + public string[] AvaloniaNameGeneratorFilterByPath => GetStringArrayProperty( + BuildProperties.AvaloniaNameGeneratorFilterByPath, + "*"); + + public string[] AvaloniaNameGeneratorFilterByNamespace => GetStringArrayProperty( + BuildProperties.AvaloniaNameGeneratorFilterByNamespace, + "*"); + + private string[] GetStringArrayProperty(BuildProperties name, string defaultValue) + { + var key = name.ToString(); + var value = _context.GetMsBuildProperty(key, defaultValue); + return value.Contains(";") ? value.Split(';') : new[] {value}; + } + + private TEnum GetEnumProperty(BuildProperties name, TEnum defaultValue) where TEnum : struct + { + var key = name.ToString(); + var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); + return Enum.TryParse(value, true, out TEnum behavior) ? behavior : defaultValue; + } + + private bool GetBoolProperty(BuildProperties name, bool defaultValue) + { + var key = name.ToString(); + var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); + return bool.TryParse(value, out var result) ? result : defaultValue; + } +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs new file mode 100644 index 0000000000..8d45afafa7 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Generators.Common.Domain; +using Microsoft.CodeAnalysis; + +namespace Avalonia.Generators.NameGenerator; + +internal class AvaloniaNameGenerator : INameGenerator +{ + private readonly ViewFileNamingStrategy _naming; + private readonly IGlobPattern _pathPattern; + private readonly IGlobPattern _namespacePattern; + private readonly IViewResolver _classes; + private readonly INameResolver _names; + private readonly ICodeGenerator _code; + + public AvaloniaNameGenerator( + ViewFileNamingStrategy naming, + IGlobPattern pathPattern, + IGlobPattern namespacePattern, + IViewResolver classes, + INameResolver names, + ICodeGenerator code) + { + _naming = naming; + _pathPattern = pathPattern; + _namespacePattern = namespacePattern; + _classes = classes; + _names = names; + _code = code; + } + + public IReadOnlyList GenerateNameReferences(IEnumerable additionalFiles) + { + var resolveViews = + from file in additionalFiles + where (file.Path.EndsWith(".xaml") || + file.Path.EndsWith(".paml") || + file.Path.EndsWith(".axaml")) && + _pathPattern.Matches(file.Path) + let xaml = file.GetText()!.ToString() + let view = _classes.ResolveView(xaml) + where view != null && _namespacePattern.Matches(view.Namespace) + select view; + + var query = + from view in resolveViews + let names = _names.ResolveNames(view.Xaml) + let code = _code.GenerateCode(view.ClassName, view.Namespace, view.XamlType, names) + let fileName = ResolveViewFileName(view, _naming) + select new GeneratedPartialClass(fileName, code); + + return query.ToList(); + } + + private static string ResolveViewFileName(ResolvedView view, ViewFileNamingStrategy strategy) => strategy switch + { + ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs", + ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs", + _ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, "Unknown naming strategy!") + }; +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs new file mode 100644 index 0000000000..fa1dcde064 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs @@ -0,0 +1,60 @@ +using System; +using Avalonia.Generators.Common; +using Avalonia.Generators.Common.Domain; +using Avalonia.Generators.Compiler; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Avalonia.Generators.NameGenerator; + +[Generator] +public class AvaloniaNameSourceGenerator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) { } + + public void Execute(GeneratorExecutionContext context) + { + try + { + var generator = CreateNameGenerator(context); + if (generator is null) + { + return; + } + + var partials = generator.GenerateNameReferences(context.AdditionalFiles); + foreach (var (fileName, content) in partials) context.AddSource(fileName, content); + } + catch (Exception exception) + { + context.ReportNameGeneratorUnhandledError(exception); + } + } + + private static INameGenerator CreateNameGenerator(GeneratorExecutionContext context) + { + var options = new GeneratorOptions(context); + if (!options.AvaloniaNameGeneratorIsEnabled) + { + return null; + } + + var types = new RoslynTypeSystem((CSharpCompilation)context.Compilation); + ICodeGenerator generator = options.AvaloniaNameGeneratorBehavior switch { + Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(), + Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types), + _ => throw new ArgumentOutOfRangeException() + }; + + var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute); + return new AvaloniaNameGenerator( + options.AvaloniaNameGeneratorViewFileNamingStrategy, + new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByPath), + new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByNamespace), + new XamlXViewResolver(types, compiler, true, + type => context.ReportNameGeneratorInvalidType(type), + error => context.ReportNameGeneratorUnhandledError(error)), + new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier), + generator); + } +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs new file mode 100644 index 0000000000..922d800332 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Avalonia.Generators.NameGenerator; + +internal interface INameGenerator +{ + IReadOnlyList GenerateNameReferences(IEnumerable additionalFiles); +} + +internal record GeneratedPartialClass(string FileName, string Content); diff --git a/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs new file mode 100644 index 0000000000..b84c65f7b5 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using Avalonia.Generators.Common.Domain; +using XamlX.TypeSystem; + +namespace Avalonia.Generators.NameGenerator; + +internal class InitializeComponentCodeGenerator: ICodeGenerator +{ + private readonly bool _diagnosticsAreConnected; + private const string AttachDevToolsCodeBlock = @" +#if DEBUG + if (attachDevTools) + { + this.AttachDevTools(); + } +#endif +"; + private const string AttachDevToolsParameterDocumentation + = @" /// Should the dev tools be attached. +"; + + public InitializeComponentCodeGenerator(IXamlTypeSystem types) + { + _diagnosticsAreConnected = types.FindAssembly("Avalonia.Diagnostics") != null; + } + + public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names) + { + var properties = new List(); + var initializations = new List(); + foreach (var resolvedName in names) + { + var (typeName, name, fieldModifier) = resolvedName; + properties.Add($" {fieldModifier} {typeName} {name};"); + initializations.Add($" {name} = this.FindNameScope()?.Find<{typeName}>(\"{name}\");"); + } + + var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType); + + return $@"// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace {nameSpace} +{{ + partial class {className} + {{ +{string.Join("\n", properties)} + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. +{(attachDevTools ? AttachDevToolsParameterDocumentation : string.Empty)} + public void InitializeComponent(bool loadXaml = true{(attachDevTools ? ", bool attachDevTools = true" : string.Empty)}) + {{ + if (loadXaml) + {{ + AvaloniaXamlLoader.Load(this); + }} +{(attachDevTools ? AttachDevToolsCodeBlock : string.Empty)} +{string.Join("\n", initializations)} + }} + }} +}} +"; + } + + private static bool IsWindow(IXamlType xamlType) + { + var type = xamlType; + bool isWindow; + do + { + isWindow = type.FullName == "Avalonia.Controls.Window"; + type = type.BaseType; + } while (!isWindow && type != null); + + return isWindow; + } +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs new file mode 100644 index 0000000000..367874e212 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Generators.Common.Domain; +using XamlX.TypeSystem; + +namespace Avalonia.Generators.NameGenerator; + +internal class OnlyPropertiesCodeGenerator : ICodeGenerator +{ + public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names) + { + var namedControls = names + .Select(info => " " + + $"{info.FieldModifier} {info.TypeName} {info.Name} => " + + $"this.FindNameScope()?.Find<{info.TypeName}>(\"{info.Name}\");") + .ToList(); + var lines = string.Join("\n", namedControls); + return $@"// + +using Avalonia.Controls; + +namespace {nameSpace} +{{ + partial class {className} + {{ +{lines} + }} +}} +"; + } +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/Options.cs b/src/tools/Avalonia.Generators/NameGenerator/Options.cs new file mode 100644 index 0000000000..abdaaab72b --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/Options.cs @@ -0,0 +1,21 @@ +namespace Avalonia.Generators.NameGenerator; + +internal enum Options +{ + Public = 0, + Private = 1, + Internal = 2, + Protected = 3, +} + +internal enum Behavior +{ + OnlyProperties = 0, + InitializeComponent = 1, +} + +internal enum ViewFileNamingStrategy +{ + ClassName = 0, + NamespaceAndClassName = 1, +} diff --git a/src/tools/Avalonia.Generators/Properties/launchSettings.json b/src/tools/Avalonia.Generators/Properties/launchSettings.json new file mode 100644 index 0000000000..2066a00fc5 --- /dev/null +++ b/src/tools/Avalonia.Generators/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Profile 1": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\..\\..\\samples\\Generators.Sandbox\\Generators.Sandbox.csproj" + } + } +} \ No newline at end of file diff --git a/src/tools/Avalonia.Generators/README.md b/src/tools/Avalonia.Generators/README.md new file mode 100644 index 0000000000..73e9e71196 --- /dev/null +++ b/src/tools/Avalonia.Generators/README.md @@ -0,0 +1,209 @@ +[![NuGet Stats](https://img.shields.io/nuget/v/XamlNameReferenceGenerator.svg)](https://www.nuget.org/packages/XamlNameReferenceGenerator) [![downloads](https://img.shields.io/nuget/dt/XamlNameReferenceGenerator)](https://www.nuget.org/packages/XamlNameReferenceGenerator) ![Build](https://github.com/avaloniaui/Avalonia.NameGenerator/workflows/Build/badge.svg) ![License](https://img.shields.io/github/license/avaloniaui/Avalonia.NameGenerator.svg) ![Size](https://img.shields.io/github/repo-size/avaloniaui/Avalonia.NameGenerator.svg) + +### C# `SourceGenerator` for Typed Avalonia `x:Name` References + +This is a [C# `SourceGenerator`](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) built for generating strongly-typed references to controls with `x:Name` (or just `Name`) attributes declared in XAML (or, in `.axaml`). The source generator will look for the `xaml` (or `axaml`) file with the same name as your partial C# class that is a subclass of `Avalonia.INamed` and parses the XAML markup, finds all XAML tags with `x:Name` attributes and generates the C# code. + +### Getting Started + +In order to get started, just install the NuGet package: + +``` +dotnet add package XamlNameReferenceGenerator +``` + +Or, if you are using [submodules](https://git-scm.com/docs/git-submodule), you can reference the generator as such: + +```xml + + + + + +``` + +### Usage + +After installing the NuGet package, declare your view class as `partial`. Typed C# references to Avalonia controls declared in XAML files will be generated for classes referenced by the `x:Class` directive in XAML files. For example, for the following XAML markup: + +```xml + + + +``` + +A new C# partial class named `SignUpView` with a single `public` property named `UserNameTextBox` of type `TextBox` will be generated in the `Sample.App` namespace. We won't see the generated file, but we'll be able to access the generated property as shown below: + +```cs +using Avalonia.Controls; + +namespace Sample.App +{ + public partial class SignUpView : Window + { + public SignUpView() + { + // This method is generated. Call it before accessing any + // of the generated properties. The 'UserNameTextBox' + // property is also generated. + InitializeComponent(); + UserNameTextBox.Text = "Joseph"; + } + } +} +``` + + + +### Why do I need this? + +The typed `x:Name` references might be useful if you decide to use e.g. [ReactiveUI code-behind bindings](https://www.reactiveui.net/docs/handbook/data-binding/): + +```cs +// UserNameValidation and PasswordValidation are auto generated. +public partial class SignUpView : ReactiveWindow +{ + public SignUpView() + { + InitializeComponent(); + this.WhenActivated(disposables => + { + this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text) + .DisposeWith(disposables); + this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text) + .DisposeWith(disposables); + }); + } +} +``` + +### Advanced Usage + +> Never keep a method named `InitializeComponent` in your code-behind view class if you are using the generator with `AvaloniaNameGeneratorBehavior` set to `InitializeComponent` (this is the default value). The private `InitializeComponent` method declared in your code-behind class hides the `InitializeComponent` method generated by `Avalonia.NameGenerator`, see [Issue 69](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/69). If you wish to use your own `InitializeComponent` method (not the generated one), set `AvaloniaNameGeneratorBehavior` to `OnlyProperties`. + +The `x:Name` generator can be configured via MsBuild properties that you can put into your C# project file (`.csproj`). Using such options, you can configure the generator behavior, the default field modifier, namespace and path filters. The generator supports the following options: + +- `AvaloniaNameGeneratorBehavior` + Possible values: `OnlyProperties`, `InitializeComponent` + Default value: `InitializeComponent` + Determines if the generator should generate get-only properties, or the `InitializeComponent` method. + +- `AvaloniaNameGeneratorDefaultFieldModifier` + Possible values: `internal`, `public`, `private`, `protected` + Default value: `internal` + The default field modifier that should be used when there is no `x:FieldModifier` directive specified. + +- `AvaloniaNameGeneratorFilterByPath` + Posssible format: `glob_pattern`, `glob_pattern;glob_pattern` + Default value: `*` + The generator will process only XAML files with paths matching the specified glob pattern(s). + Example: `*/Views/*View.xaml`, `*View.axaml;*Control.axaml` + +- `AvaloniaNameGeneratorFilterByNamespace` + Posssible format: `glob_pattern`, `glob_pattern;glob_pattern` + Default value: `*` + The generator will process only XAML files with base classes' namespaces matching the specified glob pattern(s). + Example: `MyApp.Presentation.*`, `MyApp.Presentation.Views;MyApp.Presentation.Controls` + +- `AvaloniaNameGeneratorViewFileNamingStrategy` + Possible values: `ClassName`, `NamespaceAndClassName` + Default value: `NamespaceAndClassName` + Determines how the automatically generated view files should be [named](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/92). + +The default values are given by: + +```xml + + + InitializeComponent + internal + * + * + NamespaceAndClassName + + + +``` + +![](https://user-images.githubusercontent.com/6759207/107812261-7ddfea00-6d80-11eb-9c7e-67bf95d0f0d4.gif) + +### What do the generated sources look like? + +For [`SignUpView`](https://github.com/avaloniaui/Avalonia.NameGenerator/blob/main/src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml), we get the following generated output when the source generator is in the `InitializeComponent` mode: + +```cs +// + +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox; + public global::Avalonia.Controls.TextBlock UserNameValidation; + private global::Avalonia.Controls.TextBox PasswordTextBox; + internal global::Avalonia.Controls.TextBlock PasswordValidation; + internal global::Avalonia.Controls.ListBox AwesomeListView; + internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; + internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation; + internal global::Avalonia.Controls.Button SignUpButton; + internal global::Avalonia.Controls.TextBlock CompoundValidation; + + public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + +// This will be added only if you install Avalonia.Diagnostics. +#if DEBUG + if (attachDevTools) + { + this.AttachDevTools(); + } +#endif + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + UserNameValidation = this.FindNameScope()?.Find("UserNameValidation"); + PasswordTextBox = this.FindNameScope()?.Find("PasswordTextBox"); + PasswordValidation = this.FindNameScope()?.Find("PasswordValidation"); + AwesomeListView = this.FindNameScope()?.Find("AwesomeListView"); + ConfirmPasswordTextBox = this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + ConfirmPasswordValidation = this.FindNameScope()?.Find("ConfirmPasswordValidation"); + SignUpButton = this.FindNameScope()?.Find("SignUpButton"); + CompoundValidation = this.FindNameScope()?.Find("CompoundValidation"); + } + } +} +``` + +If you enable the `OnlyProperties` source generator mode, you get: + +```cs +// + +using Avalonia.Controls; + +namespace Avalonia.NameGenerator.Sandbox.Views +{ + partial class SignUpView + { + internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + public global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find("UserNameValidation"); + private global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find("PasswordTextBox"); + internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find("PasswordValidation"); + internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find("ConfirmPasswordValidation"); + internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find("SignUpButton"); + internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find("CompoundValidation"); + } +} +``` diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs index 11ecac0039..89e609eb10 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs @@ -16,9 +16,11 @@ namespace Avalonia.Base.UnitTests.Media var typeface = new Typeface(fontFamily); - var glyphTypeface = FontManager.Current.GetOrAddGlyphTypeface(typeface); + Assert.True(FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)); - Assert.Same(glyphTypeface, FontManager.Current.GetOrAddGlyphTypeface(typeface)); + FontManager.Current.TryGetGlyphTypeface(typeface, out var other); + + Assert.Same(glyphTypeface, other); } } diff --git a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs b/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs index afc25ab88e..82dcd8f4fc 100644 --- a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs @@ -46,9 +46,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Single_FontAsset() { var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(source); Assert.Single(fontAssets); } @@ -57,9 +56,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Single_FontAsset_Avares_Without_BaseUri() { var source = new Uri(AssetYourFontAvares); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(source); Assert.Single(fontAssets); } @@ -69,9 +67,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts { var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute); var baseUri = new Uri(AssetLocationAvares); - var key = new FontFamilyKey(source, baseUri); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(new Uri(baseUri, source)); Assert.Single(fontAssets); } @@ -80,9 +77,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Matching_Assets() { var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray(); + var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray(); foreach (var fontAsset in fontAssets) { @@ -99,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Media.Fonts { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var fontFamily = new FontFamily("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono"); + var source = new Uri("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono", UriKind.RelativeOrAbsolute); - var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key).ToArray(); + var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray(); Assert.NotEmpty(fontAssets); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 2ca716fa8f..eee6e49617 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -100,6 +100,31 @@ namespace Avalonia.Controls.UnitTests Assert.All(items, x => Assert.Same(theme, x.ItemContainerTheme)); } + [Fact] + public void Finds_Correct_DataTemplate_When_Application_DataTemplate_Is_Present() + { + // #10398 + using var app = UnitTestApplication.Start(); + + Avalonia.Application.Current.DataTemplates.Add(new FuncDataTemplate((x, _) => new Canvas())); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Avalonia.Application.Current); + + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = CreateTestTreeData(), + }; + + var root = new TestRoot(target); + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); + Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1)); + Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2)); + } + [Fact] public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers() { diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index c6ecc0a7e5..14e48b3b6c 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -1,4 +1,5 @@ -using Avalonia.Direct2D1.Media; +using System; +using Avalonia.Direct2D1.Media; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -16,18 +17,10 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("A, B, Arial"))); - - var font = glyphTypeface.DWFont; - - Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); - - Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); + var glyphTypeface = + new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface; - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Equal("Arial", glyphTypeface.FamilyName); } } @@ -38,18 +31,13 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold)); + var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface; - var font = glyphTypeface.DWFont; + Assert.Equal("Arial", glyphTypeface.FamilyName); - Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal(FontWeight.Bold, glyphTypeface.Weight); - Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight); - - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Equal(FontStyle.Normal, glyphTypeface.Style); } } @@ -60,20 +48,11 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); + var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface; - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("Unknown"))); + var defaultName = FontManager.Current.DefaultFontFamilyName; - var font = glyphTypeface.DWFont; - - var defaultName = fontManager.GetDefaultFontFamilyName(); - - Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0)); - - Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); - - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Equal(defaultName, glyphTypeface.FamilyName); } } @@ -86,12 +65,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media var fontManager = new FontManagerImpl(); - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri)); + var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface; - var font = glyphTypeface.DWFont; - - Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); } } @@ -102,14 +78,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black)); - - var font = glyphTypeface.DWFont; + var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface; - Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); } } } diff --git a/tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj b/tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj new file mode 100644 index 0000000000..ec4cbcafb7 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj @@ -0,0 +1,26 @@ + + + Exe + net6.0 + Avalonia.Generators.Tests + true + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Generators.Tests/GlobPatternTests.cs b/tests/Avalonia.Generators.Tests/GlobPatternTests.cs new file mode 100644 index 0000000000..5358c2bdda --- /dev/null +++ b/tests/Avalonia.Generators.Tests/GlobPatternTests.cs @@ -0,0 +1,31 @@ +using Avalonia.Generators.Common; +using Xunit; + +namespace Avalonia.Generators.Tests; + +public class GlobPatternTests +{ + [Theory] + [InlineData("*", "anything", true)] + [InlineData("", "anything", false)] + [InlineData("Views/*", "Views/SignUpView.xaml", true)] + [InlineData("Views/*", "Extensions/SignUpView.xaml", false)] + [InlineData("*SignUpView*", "Extensions/SignUpView.xaml", true)] + [InlineData("*SignUpView.paml", "Extensions/SignUpView.xaml", false)] + [InlineData("*.xaml", "Extensions/SignUpView.xaml", true)] + public void Should_Match_Glob_Expressions(string pattern, string value, bool matches) + { + Assert.Equal(matches, new GlobPattern(pattern).Matches(value)); + } + + [Theory] + [InlineData("Views/SignUpView.xaml", true, new[] { "*.xaml", "Extensions/*" })] + [InlineData("Extensions/SignUpView.paml", true, new[] { "*.xaml", "Extensions/*" })] + [InlineData("Extensions/SignUpView.paml", false, new[] { "*.xaml", "Views/*" })] + [InlineData("anything", true, new[] { "*", "*" })] + [InlineData("anything", false, new[] { "", "" })] + public void Should_Match_Glob_Pattern_Groups(string value, bool matches, string[] patterns) + { + Assert.Equal(matches, new GlobPatternGroup(patterns).Matches(value)); + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedProps.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedProps.txt new file mode 100644 index 0000000000..42f6801af0 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedProps.txt @@ -0,0 +1,28 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedPropsWithDevTools.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedPropsWithDevTools.txt new file mode 100644 index 0000000000..c7ca9f20c1 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedPropsWithDevTools.txt @@ -0,0 +1,36 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + /// Should the dev tools be attached. + + public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + +#if DEBUG + if (attachDevTools) + { + this.AttachDevTools(); + } +#endif + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/ControlWithoutWindow.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/ControlWithoutWindow.txt new file mode 100644 index 0000000000..42f6801af0 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/ControlWithoutWindow.txt @@ -0,0 +1,28 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/CustomControls.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/CustomControls.txt new file mode 100644 index 0000000000..2e9a534b3a --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/CustomControls.txt @@ -0,0 +1,32 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.ReactiveUI.RoutedViewHost ClrNamespaceRoutedViewHost; + internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost; + internal global::Controls.CustomTextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + ClrNamespaceRoutedViewHost = this.FindNameScope()?.Find("ClrNamespaceRoutedViewHost"); + UriRoutedViewHost = this.FindNameScope()?.Find("UriRoutedViewHost"); + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/DataTemplates.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/DataTemplates.txt new file mode 100644 index 0000000000..fff718517c --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/DataTemplates.txt @@ -0,0 +1,30 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + internal global::Avalonia.Controls.ListBox NamedListBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + NamedListBox = this.FindNameScope()?.Find("NamedListBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/FieldModifier.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/FieldModifier.txt new file mode 100644 index 0000000000..b0ba74ca17 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/FieldModifier.txt @@ -0,0 +1,38 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + public global::Avalonia.Controls.TextBox FirstNameTextBox; + public global::Avalonia.Controls.TextBox LastNameTextBox; + protected global::Avalonia.Controls.TextBox PasswordTextBox; + private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; + internal global::Avalonia.Controls.Button SignUpButton; + internal global::Avalonia.Controls.Button RegisterButton; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + FirstNameTextBox = this.FindNameScope()?.Find("FirstNameTextBox"); + LastNameTextBox = this.FindNameScope()?.Find("LastNameTextBox"); + PasswordTextBox = this.FindNameScope()?.Find("PasswordTextBox"); + ConfirmPasswordTextBox = this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + SignUpButton = this.FindNameScope()?.Find("SignUpButton"); + RegisterButton = this.FindNameScope()?.Find("RegisterButton"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/InitializeComponentCode.cs b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/InitializeComponentCode.cs new file mode 100644 index 0000000000..41d2cfaaee --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/InitializeComponentCode.cs @@ -0,0 +1,35 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent; + +public static class InitializeComponentCode +{ + public const string NamedControl = "NamedControl.txt"; + public const string NamedControls = "NamedControls.txt"; + public const string XNamedControl = "xNamedControl.txt"; + public const string XNamedControls = "xNamedControls.txt"; + public const string NoNamedControls = "NoNamedControls.txt"; + public const string CustomControls = "CustomControls.txt"; + public const string DataTemplates = "DataTemplates.txt"; + public const string SignUpView = "SignUpView.txt"; + public const string FieldModifier = "FieldModifier.txt"; + public const string AttachedProps = "AttachedProps.txt"; + public const string AttachedPropsWithDevTools = "AttachedPropsWithDevTools.txt"; + public const string ControlWithoutWindow = "ControlWithoutWindow.txt"; + + public static async Task Load(string generatedCodeResourceName) + { + var assembly = typeof(XamlXNameResolverTests).Assembly; + var fullResourceName = assembly + .GetManifestResourceNames() + .First(name => name.Contains("InitializeComponent") && + name.Contains("GeneratedInitializeComponent") && + name.EndsWith(generatedCodeResourceName)); + + await using var stream = assembly.GetManifestResourceStream(fullResourceName); + using var reader = new StreamReader(stream!); + return await reader.ReadToEndAsync(); + } +} \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControl.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControl.txt new file mode 100644 index 0000000000..42f6801af0 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControl.txt @@ -0,0 +1,28 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControls.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControls.txt new file mode 100644 index 0000000000..3451718ce5 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControls.txt @@ -0,0 +1,32 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + internal global::Avalonia.Controls.TextBox PasswordTextBox; + internal global::Avalonia.Controls.Button SignUpButton; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + PasswordTextBox = this.FindNameScope()?.Find("PasswordTextBox"); + SignUpButton = this.FindNameScope()?.Find("SignUpButton"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NoNamedControls.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NoNamedControls.txt new file mode 100644 index 0000000000..b68dce6170 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NoNamedControls.txt @@ -0,0 +1,28 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/SignUpView.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/SignUpView.txt new file mode 100644 index 0000000000..541a6f7106 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/SignUpView.txt @@ -0,0 +1,46 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Controls.CustomTextBox UserNameTextBox; + internal global::Avalonia.Controls.TextBlock UserNameValidation; + internal global::Avalonia.Controls.TextBox PasswordTextBox; + internal global::Avalonia.Controls.TextBlock PasswordValidation; + internal global::Avalonia.Controls.ListBox AwesomeListView; + internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; + internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation; + internal global::Avalonia.Controls.Documents.Run SignUpButtonDescription; + internal global::Avalonia.Controls.Button SignUpButton; + internal global::Avalonia.Controls.TextBlock CompoundValidation; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + UserNameValidation = this.FindNameScope()?.Find("UserNameValidation"); + PasswordTextBox = this.FindNameScope()?.Find("PasswordTextBox"); + PasswordValidation = this.FindNameScope()?.Find("PasswordValidation"); + AwesomeListView = this.FindNameScope()?.Find("AwesomeListView"); + ConfirmPasswordTextBox = this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + ConfirmPasswordValidation = this.FindNameScope()?.Find("ConfirmPasswordValidation"); + SignUpButtonDescription = this.FindNameScope()?.Find("SignUpButtonDescription"); + SignUpButton = this.FindNameScope()?.Find("SignUpButton"); + CompoundValidation = this.FindNameScope()?.Find("CompoundValidation"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControl.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControl.txt new file mode 100644 index 0000000000..42f6801af0 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControl.txt @@ -0,0 +1,28 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControls.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControls.txt new file mode 100644 index 0000000000..3451718ce5 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControls.txt @@ -0,0 +1,32 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + internal global::Avalonia.Controls.TextBox PasswordTextBox; + internal global::Avalonia.Controls.Button SignUpButton; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + PasswordTextBox = this.FindNameScope()?.Find("PasswordTextBox"); + SignUpButton = this.FindNameScope()?.Find("SignUpButton"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs b/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs new file mode 100644 index 0000000000..7bd9c35577 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs @@ -0,0 +1,63 @@ +using System.Threading.Tasks; +using Avalonia.Generators.Common; +using Avalonia.Generators.Compiler; +using Avalonia.Generators.NameGenerator; +using Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent; +using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; +using Avalonia.Generators.Tests.Views; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Avalonia.Generators.Tests.InitializeComponent; + +public class InitializeComponentTests +{ + [Theory] + [InlineData(InitializeComponentCode.NamedControl, View.NamedControl, false)] + [InlineData(InitializeComponentCode.NamedControls, View.NamedControls, false)] + [InlineData(InitializeComponentCode.XNamedControl, View.XNamedControl, false)] + [InlineData(InitializeComponentCode.XNamedControls, View.XNamedControls, false)] + [InlineData(InitializeComponentCode.NoNamedControls, View.NoNamedControls, false)] + [InlineData(InitializeComponentCode.CustomControls, View.CustomControls, false)] + [InlineData(InitializeComponentCode.DataTemplates, View.DataTemplates, false)] + [InlineData(InitializeComponentCode.SignUpView, View.SignUpView, false)] + [InlineData(InitializeComponentCode.FieldModifier, View.FieldModifier, false)] + [InlineData(InitializeComponentCode.AttachedPropsWithDevTools, View.AttachedProps, true)] + [InlineData(InitializeComponentCode.AttachedProps, View.AttachedProps, false)] + [InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, true)] + [InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, false)] + public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File( + string expectation, + string markup, + bool devToolsMode) + { + var excluded = devToolsMode ? null : "Avalonia.Diagnostics"; + var compilation = + View.CreateAvaloniaCompilation(excluded) + .WithCustomTextBox(); + + var types = new RoslynTypeSystem(compilation); + var classResolver = new XamlXViewResolver( + types, + MiniCompiler.CreateDefault( + new RoslynTypeSystem(compilation), + MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); + + var xaml = await View.Load(markup); + var classInfo = classResolver.ResolveView(xaml); + var nameResolver = new XamlXNameResolver(); + var names = nameResolver.ResolveNames(classInfo.Xaml); + + var generator = new InitializeComponentCodeGenerator(types); + + var code = generator + .GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) + .Replace("\r", string.Empty); + + var expected = await InitializeComponentCode.Load(expectation); + + + CSharpSyntaxTree.ParseText(code); + Assert.Equal(expected.Replace("\r", string.Empty), code); + } +} diff --git a/tests/Avalonia.Generators.Tests/MiniCompilerTests.cs b/tests/Avalonia.Generators.Tests/MiniCompilerTests.cs new file mode 100644 index 0000000000..26ed20f982 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/MiniCompilerTests.cs @@ -0,0 +1,59 @@ +using System; +using System.ComponentModel; +using Avalonia.Generators.Compiler; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Avalonia.Generators.Tests.Views; +using XamlX; +using XamlX.Parsers; +using Xunit; + +namespace Avalonia.Generators.Tests; + +public class MiniCompilerTests +{ + private const string AvaloniaXaml = ""; + private const string MiniClass = "namespace Example { public class Valid { public int Foo() => 21; } }"; + private const string MiniInvalidXaml = ""; + private const string MiniValidXaml = ""; + + [Fact] + public void Should_Resolve_Types_From_Simple_Valid_Xaml_Markup() + { + var xaml = XDocumentXamlParser.Parse(MiniValidXaml); + var compilation = CreateBasicCompilation(MiniClass); + MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); + + Assert.NotNull(xaml.Root); + } + + [Fact] + public void Should_Throw_When_Unable_To_Resolve_Types_From_Simple_Invalid_Markup() + { + var xaml = XDocumentXamlParser.Parse(MiniInvalidXaml); + var compilation = CreateBasicCompilation(MiniClass); + var compiler = MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)); + + Assert.Throws(() => compiler.Transform(xaml)); + } + + [Fact] + public void Should_Resolve_Types_From_Simple_Avalonia_Markup() + { + var xaml = XDocumentXamlParser.Parse(AvaloniaXaml); + var compilation = View.CreateAvaloniaCompilation(); + MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); + + Assert.NotNull(xaml.Root); + } + + private static CSharpCompilation CreateBasicCompilation(string source) => + CSharpCompilation + .Create("BasicLib", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) + .AddReferences(MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location)) + .AddReferences(MetadataReference.CreateFromFile(typeof(ITypeDescriptorContext).Assembly.Location)) + .AddReferences(MetadataReference.CreateFromFile(typeof(ISupportInitialize).Assembly.Location)) + .AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverterAttribute).Assembly.Location)) + .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)); +} \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/AttachedProps.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/AttachedProps.txt new file mode 100644 index 0000000000..8a3a65773c --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/AttachedProps.txt @@ -0,0 +1,11 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/ControlWithoutWindow.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/ControlWithoutWindow.txt new file mode 100644 index 0000000000..8a3a65773c --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/ControlWithoutWindow.txt @@ -0,0 +1,11 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/CustomControls.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/CustomControls.txt new file mode 100644 index 0000000000..d9328b4b0d --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/CustomControls.txt @@ -0,0 +1,13 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.ReactiveUI.RoutedViewHost ClrNamespaceRoutedViewHost => this.FindNameScope()?.Find("ClrNamespaceRoutedViewHost"); + internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost => this.FindNameScope()?.Find("UriRoutedViewHost"); + internal global::Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/DataTemplates.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/DataTemplates.txt new file mode 100644 index 0000000000..ee73a529e9 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/DataTemplates.txt @@ -0,0 +1,12 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + internal global::Avalonia.Controls.ListBox NamedListBox => this.FindNameScope()?.Find("NamedListBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/FieldModifier.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/FieldModifier.txt new file mode 100644 index 0000000000..250e8c98f3 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/FieldModifier.txt @@ -0,0 +1,16 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + public global::Avalonia.Controls.TextBox FirstNameTextBox => this.FindNameScope()?.Find("FirstNameTextBox"); + public global::Avalonia.Controls.TextBox LastNameTextBox => this.FindNameScope()?.Find("LastNameTextBox"); + protected global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find("PasswordTextBox"); + private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find("SignUpButton"); + internal global::Avalonia.Controls.Button RegisterButton => this.FindNameScope()?.Find("RegisterButton"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControl.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControl.txt new file mode 100644 index 0000000000..8a3a65773c --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControl.txt @@ -0,0 +1,11 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControls.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControls.txt new file mode 100644 index 0000000000..1129600cea --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControls.txt @@ -0,0 +1,13 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find("PasswordTextBox"); + internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find("SignUpButton"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NoNamedControls.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NoNamedControls.txt new file mode 100644 index 0000000000..7db25c4693 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NoNamedControls.txt @@ -0,0 +1,11 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/OnlyPropertiesCode.cs b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/OnlyPropertiesCode.cs new file mode 100644 index 0000000000..3a69735bb3 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/OnlyPropertiesCode.cs @@ -0,0 +1,33 @@ +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; + +public static class OnlyPropertiesCode +{ + public const string NamedControl = "NamedControl.txt"; + public const string NamedControls = "NamedControls.txt"; + public const string XNamedControl = "xNamedControl.txt"; + public const string XNamedControls = "xNamedControls.txt"; + public const string NoNamedControls = "NoNamedControls.txt"; + public const string CustomControls = "CustomControls.txt"; + public const string DataTemplates = "DataTemplates.txt"; + public const string SignUpView = "SignUpView.txt"; + public const string AttachedProps = "AttachedProps.txt"; + public const string FieldModifier = "FieldModifier.txt"; + public const string ControlWithoutWindow = "ControlWithoutWindow.txt"; + + public static async Task Load(string generatedCodeResourceName) + { + var assembly = typeof(XamlXNameResolverTests).Assembly; + var fullResourceName = assembly + .GetManifestResourceNames() + .First(name => name.Contains("OnlyProperties") && name.EndsWith(generatedCodeResourceName)); + + await using var stream = assembly.GetManifestResourceStream(fullResourceName); + using var reader = new StreamReader(stream!); + return await reader.ReadToEndAsync(); + } +} \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/SignUpView.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/SignUpView.txt new file mode 100644 index 0000000000..c70abaf6af --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/SignUpView.txt @@ -0,0 +1,20 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + internal global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find("UserNameValidation"); + internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find("PasswordTextBox"); + internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find("PasswordValidation"); + internal global::Avalonia.Controls.ListBox AwesomeListView => this.FindNameScope()?.Find("AwesomeListView"); + internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find("ConfirmPasswordValidation"); + internal global::Avalonia.Controls.Documents.Run SignUpButtonDescription => this.FindNameScope()?.Find("SignUpButtonDescription"); + internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find("SignUpButton"); + internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find("CompoundValidation"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControl.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControl.txt new file mode 100644 index 0000000000..8a3a65773c --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControl.txt @@ -0,0 +1,11 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControls.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControls.txt new file mode 100644 index 0000000000..1129600cea --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControls.txt @@ -0,0 +1,13 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find("PasswordTextBox"); + internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find("SignUpButton"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs b/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs new file mode 100644 index 0000000000..937fd323b7 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using Avalonia.Generators.Common; +using Avalonia.Generators.Compiler; +using Avalonia.Generators.NameGenerator; +using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; +using Avalonia.Generators.Tests.Views; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Avalonia.Generators.Tests.OnlyProperties; + +public class OnlyPropertiesTests +{ + [Theory] + [InlineData(OnlyPropertiesCode.NamedControl, View.NamedControl)] + [InlineData(OnlyPropertiesCode.NamedControls, View.NamedControls)] + [InlineData(OnlyPropertiesCode.XNamedControl, View.XNamedControl)] + [InlineData(OnlyPropertiesCode.XNamedControls, View.XNamedControls)] + [InlineData(OnlyPropertiesCode.NoNamedControls, View.NoNamedControls)] + [InlineData(OnlyPropertiesCode.CustomControls, View.CustomControls)] + [InlineData(OnlyPropertiesCode.DataTemplates, View.DataTemplates)] + [InlineData(OnlyPropertiesCode.SignUpView, View.SignUpView)] + [InlineData(OnlyPropertiesCode.AttachedProps, View.AttachedProps)] + [InlineData(OnlyPropertiesCode.FieldModifier, View.FieldModifier)] + [InlineData(OnlyPropertiesCode.ControlWithoutWindow, View.ControlWithoutWindow)] + public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(string expectation, string markup) + { + var compilation = + View.CreateAvaloniaCompilation() + .WithCustomTextBox(); + + var classResolver = new XamlXViewResolver( + new RoslynTypeSystem(compilation), + MiniCompiler.CreateDefault( + new RoslynTypeSystem(compilation), + MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); + + var xaml = await View.Load(markup); + var classInfo = classResolver.ResolveView(xaml); + var nameResolver = new XamlXNameResolver(); + var names = nameResolver.ResolveNames(classInfo.Xaml); + + var generator = new OnlyPropertiesCodeGenerator(); + var code = generator + .GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) + .Replace("\r", string.Empty); + + var expected = await OnlyPropertiesCode.Load(expectation); + CSharpSyntaxTree.ParseText(code); + Assert.Equal(expected.Replace("\r", string.Empty), code); + } +} diff --git a/tests/Avalonia.Generators.Tests/Views/AttachedProps.xml b/tests/Avalonia.Generators.Tests/Views/AttachedProps.xml new file mode 100644 index 0000000000..896da6d1cd --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Views/AttachedProps.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/Views/ControlWithoutWindow.xml b/tests/Avalonia.Generators.Tests/Views/ControlWithoutWindow.xml new file mode 100644 index 0000000000..77de06a27e --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Views/ControlWithoutWindow.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/Views/CustomControls.xml b/tests/Avalonia.Generators.Tests/Views/CustomControls.xml new file mode 100644 index 0000000000..9085e73d4b --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Views/CustomControls.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/Views/DataTemplates.xml b/tests/Avalonia.Generators.Tests/Views/DataTemplates.xml new file mode 100644 index 0000000000..f7e15644aa --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Views/DataTemplates.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/Views/FieldModifier.xml b/tests/Avalonia.Generators.Tests/Views/FieldModifier.xml new file mode 100644 index 0000000000..3ee5e51466 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Views/FieldModifier.xml @@ -0,0 +1,28 @@ + + + + + + +