diff --git a/.editorconfig b/.editorconfig index 30edee1633..128b6f1712 100644 --- a/.editorconfig +++ b/.editorconfig @@ -137,6 +137,9 @@ space_within_single_line_array_initializer_braces = true # Wrapping preferences csharp_wrap_before_ternary_opsigns = false +# Avalonia DevAnalyzer preferences +dotnet_diagnostic.AVADEV2001.severity = error + # Xaml files [*.{xaml,axaml}] indent_size = 2 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 46e8665945..2f63750cdc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -21,7 +21,7 @@ - [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Documentation with user documentation ## Breaking changes - + ## Obsoletions / Deprecations diff --git a/.gitignore b/.gitignore index 44fe5e4ba4..84faae1806 100644 --- a/.gitignore +++ b/.gitignore @@ -212,3 +212,6 @@ coc-settings.json *.map src/Web/Avalonia.Web.Blazor/wwwroot/*.js src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js +node_modules +src/Web/Avalonia.Web.Blazor/webapp/package-lock.json +src/Web/Avalonia.Web.Blazor/wwwroot diff --git a/Avalonia.sln b/Avalonia.sln index 4999719676..35b6b2108a 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -13,7 +13,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1", "src\W EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls", "src\Avalonia.Controls\Avalonia.Controls.csproj", "{D2221C82-4A25-4583-9B43-D791E3F6820C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Default", "src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Simple", "src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Diagnostics", "src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj", "{7062AE20-5DCC-4442-9645-8195BDECE63E}" EndProject diff --git a/azure-pipelines.yml b/azure-pipelines.yml index edf3c3d819..52fc8db53c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macOS-10.15' + vmImage: 'macos-12' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' @@ -91,10 +91,10 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx11.1' + sdk: 'macosx12.3' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: '12' # Options: 8, 9, default, specifyPath + xcodeVersion: '13' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 85e7a1f34d..620ec58ff3 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props index c3b136d41d..1911c02677 100644 --- a/build/ReactiveUI.props +++ b/build/ReactiveUI.props @@ -1,5 +1,5 @@ - + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 3d9548ab9d..1b60bb4df9 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Avalonia - 0.10.999 + 11.0.999 Copyright 2022 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index d54cffba08..31619399f9 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 95f61422cb..ddc50c26b6 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -91,8 +91,6 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) { if(_parent != nullptr) { _parent->_children.remove(this); - - _parent->BringToFront(); } auto cparent = dynamic_cast(parent); @@ -121,7 +119,7 @@ void WindowImpl::BringToFront() { if(Window != nullptr) { - if (![Window isMiniaturized]) + if ([Window isVisible] && ![Window isMiniaturized]) { if(IsDialog()) { diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 9fcb9d6b7f..4bbb667154 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -36,25 +36,6 @@ partial class Build : NukeBuild { [Solution("Avalonia.sln")] readonly Solution Solution; - static Lazy MsBuildExe = new Lazy(() => - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return null; - - var msBuildDirectory = VSWhere("-latest -nologo -property installationPath -format value -prerelease").FirstOrDefault().Text; - - if (!string.IsNullOrWhiteSpace(msBuildDirectory)) - { - string msBuildExe = Path.Combine(msBuildDirectory, @"MSBuild\Current\Bin\MSBuild.exe"); - if (!System.IO.File.Exists(msBuildExe)) - msBuildExe = Path.Combine(msBuildDirectory, @"MSBuild\15.0\Bin\MSBuild.exe"); - - return msBuildExe; - } - - return null; - }, false); - BuildParameters Parameters { get; set; } protected override void OnBuildInitialized() { @@ -89,25 +70,28 @@ partial class Build : NukeBuild } ExecWait("dotnet version:", "dotnet", "--info"); ExecWait("dotnet workloads:", "dotnet", "workload list"); + Information("Processor count: " + Environment.ProcessorCount); + Information("Available RAM: " + GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / 0x100000 + "MB"); } - IReadOnlyCollection MsBuildCommon( - string projectFile, - Configure configurator = null) + DotNetConfigHelper ApplySettingCore(DotNetConfigHelper c) { - return MSBuild(c => c - .SetProjectFile(projectFile) - // This is required for VS2019 image on Azure Pipelines - .When(Parameters.IsRunningOnWindows && - Parameters.IsRunningOnAzure, _ => _ - .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_11_X64"))) - .AddProperty("PackageVersion", Parameters.Version) + if (Parameters.IsRunningOnAzure) + c.AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_11_X64")); + c.AddProperty("PackageVersion", Parameters.Version) .AddProperty("iOSRoslynPathHackRequired", true) - .SetProcessToolPath(MsBuildExe.Value) .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal) - .Apply(configurator)); + .SetVerbosity(DotNetVerbosity.Minimal); + return c; } + DotNetBuildSettings ApplySetting(DotNetBuildSettings c, Configure configurator = null) => + ApplySettingCore(c).Build.Apply(configurator); + + DotNetPackSettings ApplySetting(DotNetPackSettings c, Configure configurator = null) => + ApplySettingCore(c).Pack.Apply(configurator); + + DotNetTestSettings ApplySetting(DotNetTestSettings c, Configure configurator = null) => + ApplySettingCore(c).Test.Apply(configurator); Target Clean => _ => _.Executes(() => { @@ -149,20 +133,11 @@ partial class Build : NukeBuild Target Compile => _ => _ .DependsOn(Clean, CompileNative) .DependsOn(CompileHtmlPreviewer) - .Executes(async () => + .Executes(() => { - if (Parameters.IsRunningOnWindows) - MsBuildCommon(Parameters.MSBuildSolution, c => c - .SetProcessArgumentConfigurator(a => a.Add("/r")) - .AddTargets("Build") - ); - - else - DotNetBuild(c => c - .SetProjectFile(Parameters.MSBuildSolution) - .AddProperty("PackageVersion", Parameters.Version) - .SetConfiguration(Parameters.Configuration) - ); + DotNetBuild(c => ApplySetting(c) + .SetProjectFile(Parameters.MSBuildSolution) + ); }); void RunCoreTest(string projectName) @@ -182,9 +157,8 @@ partial class Build : NukeBuild Information($"Running for {projectName} ({fw}) ..."); - DotNetTest(c => c + DotNetTest(c => ApplySetting(c) .SetProjectFile(project) - .SetConfiguration(Parameters.Configuration) .SetFramework(fw) .EnableNoBuild() .EnableNoRestore() @@ -263,19 +237,7 @@ partial class Build : NukeBuild .Executes(() => { var data = Parameters; - var pathToProjectSource = RootDirectory / "samples" / "ControlCatalog.NetCore"; - var pathToPublish = pathToProjectSource / "bin" / data.Configuration / "publish"; - - DotNetPublish(c => c - .SetProject(pathToProjectSource / "ControlCatalog.NetCore.csproj") - .EnableNoBuild() - .SetConfiguration(data.Configuration) - .AddProperty("PackageVersion", data.Version) - .AddProperty("PublishDir", pathToPublish)); - - Zip(data.ZipCoreArtifacts, data.BinRoot); Zip(data.ZipNuGetArtifacts, data.NugetRoot); - Zip(data.ZipTargetControlCatalogNetCoreDir, pathToPublish); }); Target CreateIntermediateNugetPackages => _ => _ @@ -283,15 +245,7 @@ partial class Build : NukeBuild .After(RunTests) .Executes(() => { - if (Parameters.IsRunningOnWindows) - - MsBuildCommon(Parameters.MSBuildSolution, c => c - .AddTargets("Pack")); - else - DotNetPack(c => c - .SetProject(Parameters.MSBuildSolution) - .SetConfiguration(Parameters.Configuration) - .AddProperty("PackageVersion", Parameters.Version)); + DotNetPack(c => ApplySetting(c).SetProject(Parameters.MSBuildSolution)); }); Target CreateNugetPackages => _ => _ diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index a92c988fbd..1826623674 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -51,14 +51,12 @@ public partial class Build public AbsolutePath NugetIntermediateRoot { get; } public AbsolutePath NugetRoot { get; } public AbsolutePath ZipRoot { get; } - public AbsolutePath BinRoot { get; } public AbsolutePath TestResultsRoot { get; } public string DirSuffix { get; } public List BuildDirs { get; } public string FileZipSuffix { get; } public AbsolutePath ZipCoreArtifacts { get; } public AbsolutePath ZipNuGetArtifacts { get; } - public AbsolutePath ZipTargetControlCatalogNetCoreDir { get; } public BuildParameters(Build b) @@ -121,14 +119,12 @@ public partial class Build NugetRoot = ArtifactsDir / "nuget"; NugetIntermediateRoot = RootDirectory / "build-intermediate" / "nuget"; ZipRoot = ArtifactsDir / "zip"; - BinRoot = ArtifactsDir / "bin"; TestResultsRoot = ArtifactsDir / "test-results"; BuildDirs = GlobDirectories(RootDirectory, "**bin").Concat(GlobDirectories(RootDirectory, "**obj")).ToList(); DirSuffix = Configuration; FileZipSuffix = Version + ".zip"; ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix); ZipNuGetArtifacts = ZipRoot / ("Avalonia-NuGet-" + FileZipSuffix); - ZipTargetControlCatalogNetCoreDir = ZipRoot / ("ControlCatalog.NetCore-" + FileZipSuffix); } string GetVersion() diff --git a/nukebuild/DotNetConfigHelper.cs b/nukebuild/DotNetConfigHelper.cs new file mode 100644 index 0000000000..932525288c --- /dev/null +++ b/nukebuild/DotNetConfigHelper.cs @@ -0,0 +1,57 @@ +using System.Globalization; +using JetBrains.Annotations; +using Nuke.Common.Tools.DotNet; +// ReSharper disable ReturnValueOfPureMethodIsNotUsed + +public class DotNetConfigHelper +{ + public DotNetBuildSettings Build; + public DotNetPackSettings Pack; + public DotNetTestSettings Test; + + public DotNetConfigHelper(DotNetBuildSettings s) + { + Build = s; + } + + public DotNetConfigHelper(DotNetPackSettings s) + { + Pack = s; + } + + public DotNetConfigHelper(DotNetTestSettings s) + { + Test = s; + } + + public DotNetConfigHelper AddProperty(string key, bool value) => + AddProperty(key, value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + public DotNetConfigHelper AddProperty(string key, string value) + { + Build = Build?.AddProperty(key, value); + Pack = Pack?.AddProperty(key, value); + Test = Test?.AddProperty(key, value); + + return this; + } + + public DotNetConfigHelper SetConfiguration(string configuration) + { + Build = Build?.SetConfiguration(configuration); + Pack = Pack?.SetConfiguration(configuration); + Test = Test?.SetConfiguration(configuration); + return this; + } + + public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity) + { + Build = Build?.SetVerbosity(verbosity); + Pack = Pack?.SetVerbostiy(verbosity); + Test = Test?.SetVerbosity(verbosity); + return this; + } + + public static implicit operator DotNetConfigHelper(DotNetBuildSettings s) => new DotNetConfigHelper(s); + public static implicit operator DotNetConfigHelper(DotNetPackSettings s) => new DotNetConfigHelper(s); + public static implicit operator DotNetConfigHelper(DotNetTestSettings s) => new DotNetConfigHelper(s); +} \ No newline at end of file diff --git a/samples/BindingDemo/App.xaml b/samples/BindingDemo/App.xaml index 3e312c8685..175e838616 100644 --- a/samples/BindingDemo/App.xaml +++ b/samples/BindingDemo/App.xaml @@ -4,6 +4,6 @@ x:Class="BindingDemo.App"> - + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index d98a068d84..b1bacc6483 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -115,10 +115,6 @@ namespace ControlCatalog.NetCore UseDBusMenu = true, EnableIme = true }) - .With(new Win32PlatformOptions - { - EnableMultitouch = true - }) .UseSkia() .AfterSetup(builder => { diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 7ebb87094a..750c1082a6 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -5,7 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; -using Avalonia.Themes.Default; +using Avalonia.Themes.Simple; using Avalonia.Themes.Fluent; using ControlCatalog.ViewModels; @@ -23,9 +23,9 @@ namespace ControlCatalog Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml") }; - public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + public static readonly StyleInclude ColorPickerSimple = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { - Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml") + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml") }; public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) @@ -33,16 +33,16 @@ namespace ControlCatalog Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") }; - public static readonly StyleInclude DataGridDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + public static readonly StyleInclude DataGridSimple = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { - Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml") + Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml") }; public static FluentTheme Fluent = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); - public static SimpleTheme Default = new SimpleTheme(new Uri("avares://ControlCatalog/Styles")); + public static SimpleTheme Simple = new SimpleTheme(new Uri("avares://ControlCatalog/Styles")); - public static Styles DefaultLight = new Styles + public static Styles SimpleLight = new Styles { new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { @@ -56,10 +56,10 @@ namespace ControlCatalog { Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml") }, - Default + Simple }; - public static Styles DefaultDark = new Styles + public static Styles SimpleDark = new Styles { new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { @@ -73,7 +73,7 @@ namespace ControlCatalog { Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml") }, - Default + Simple }; public override void Initialize() diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 8358fb3cd4..2654574a3e 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -29,7 +29,7 @@ - + diff --git a/samples/ControlCatalog/Converter/HexConverter.cs b/samples/ControlCatalog/Converter/HexConverter.cs new file mode 100644 index 0000000000..31cce5ba67 --- /dev/null +++ b/samples/ControlCatalog/Converter/HexConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace ControlCatalog.Converter; + +public class HexConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var str = value?.ToString(); + if (str == null) + return AvaloniaProperty.UnsetValue; + if (int.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int x)) + return (decimal)x; + return AvaloniaProperty.UnsetValue; + + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + try + { + if (value is decimal d) + return ((int)d).ToString("X8"); + return AvaloniaProperty.UnsetValue; + } + catch + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/samples/ControlCatalog/DecoratedWindow.xaml b/samples/ControlCatalog/DecoratedWindow.xaml index 5251a2fa55..c778b31c42 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml +++ b/samples/ControlCatalog/DecoratedWindow.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.DecoratedWindow" Title="Avalonia Control Gallery" - xmlns:local="clr-namespace:ControlCatalog" HasSystemDecorations="False" Name="Window"> + xmlns:local="clr-namespace:ControlCatalog" SystemDecorations="None" Name="Window"> diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 7461e78c33..7f5a191519 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -187,8 +187,8 @@ FluentLight FluentDark - DefaultLight - DefaultDark + SimpleLight + SimpleDark ()?.GetRuntimeInfo().IsDesktop == true) { - IList tabItems = ((IList)sideBar.Items); - tabItems.Add(new TabItem() + var tabItems = (sideBar.Items as IList); + tabItems?.Add(new TabItem() { Header = "Screens", Content = new ScreenPage() @@ -36,7 +36,7 @@ namespace ControlCatalog { if (themes.SelectedItem is CatalogTheme theme) { - var themeStyle = Application.Current.Styles[0]; + var themeStyle = Application.Current!.Styles[0]; if (theme == CatalogTheme.FluentLight) { if (App.Fluent.Mode != FluentThemeMode.Light) @@ -58,19 +58,19 @@ namespace ControlCatalog Application.Current.Styles[1] = App.ColorPickerFluent; Application.Current.Styles[2] = App.DataGridFluent; } - else if (theme == CatalogTheme.DefaultLight) + else if (theme == CatalogTheme.SimpleLight) { - App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Light; - Application.Current.Styles[0] = App.DefaultLight; - Application.Current.Styles[1] = App.ColorPickerDefault; - Application.Current.Styles[2] = App.DataGridDefault; + App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Light; + Application.Current.Styles[0] = App.SimpleLight; + Application.Current.Styles[1] = App.ColorPickerSimple; + Application.Current.Styles[2] = App.DataGridSimple; } - else if (theme == CatalogTheme.DefaultDark) + else if (theme == CatalogTheme.SimpleDark) { - App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Dark; - Application.Current.Styles[0] = App.DefaultDark; - Application.Current.Styles[1] = App.ColorPickerDefault; - Application.Current.Styles[2] = App.DataGridDefault; + App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Dark; + Application.Current.Styles[0] = App.SimpleDark; + Application.Current.Styles[1] = App.ColorPickerSimple; + Application.Current.Styles[2] = App.DataGridSimple; } } }; diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 1e4bf2de38..d5513904c0 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -18,15 +18,15 @@ - - + + + Click="OnCloseClicked" /> diff --git a/samples/ControlCatalog/Models/CatalogTheme.cs b/samples/ControlCatalog/Models/CatalogTheme.cs index f7be8e0014..37224ed26e 100644 --- a/samples/ControlCatalog/Models/CatalogTheme.cs +++ b/samples/ControlCatalog/Models/CatalogTheme.cs @@ -1,14 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace ControlCatalog.Models +namespace ControlCatalog.Models { public enum CatalogTheme { FluentLight, FluentDark, - DefaultLight, - DefaultDark + SimpleLight, + SimpleDark } } diff --git a/samples/ControlCatalog/Models/Person.cs b/samples/ControlCatalog/Models/Person.cs index 2dfa02c7ed..99bc50250b 100644 --- a/samples/ControlCatalog/Models/Person.cs +++ b/samples/ControlCatalog/Models/Person.cs @@ -85,7 +85,7 @@ namespace ControlCatalog.Models } else { - if (_errorLookup.TryGetValue(propertyName, out List errorList)) + if (_errorLookup.TryGetValue(propertyName, out var errorList)) { errorList.Clear(); errorList.Add(error!); @@ -114,12 +114,12 @@ namespace ControlCatalog.Models PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - public IEnumerable? GetErrors(string propertyName) + public IEnumerable GetErrors(string? propertyName) { - if (_errorLookup.TryGetValue(propertyName, out List errorList)) + if (propertyName is { } && _errorLookup.TryGetValue(propertyName, out var errorList)) return errorList; else - return null; + return Array.Empty(); } } } diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index 46f3705ffd..faa12bc8da 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -28,7 +28,7 @@ - + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs index 7a0957b02d..bc18327f12 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -1,8 +1,6 @@ using Avalonia.Controls; using Avalonia.LogicalTree; -using Avalonia.Markup; using Avalonia.Markup.Xaml; -using Avalonia.Markup.Data; using System; using System.Collections.Generic; using System.Linq; @@ -161,23 +159,23 @@ namespace ControlCatalog.Pages private bool LastWordContains(string? searchText, string? item) { var words = searchText?.Split(' ') ?? Array.Empty(); - var options = Sentences.Select(x => x.First).ToArray(); + var options = Sentences.Select(x => x.First) + .ToArray?>(); for (var i = 0; i < words.Length; ++i) { var word = words[i]; for (var j = 0; word is { } && j < options.Length; ++j) { - var option = options[j]; - if (option == null) - continue; - - if (i == words.Length - 1) - { - options[j] = option.Value.ToLower().Contains(word.ToLower()) ? option : null; - } - else + if (options[i] is { } option) { - options[j] = option.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase) ? option.Next : null; + if (i == words.Length - 1) + { + options[j] = option.Value.ToLower().Contains(word.ToLower()) ? option : null; + } + else + { + options[j] = option.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase) ? option.Next : null; + } } } } diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs index 5c584b8781..e7450075ad 100644 --- a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs @@ -21,20 +21,23 @@ namespace ControlCatalog.Pages public void OnSpin(object sender, SpinEventArgs e) { var spinner = (ButtonSpinner)sender; - var txtBox = (TextBlock)spinner.Content; - int value = Array.IndexOf(_mountains, txtBox?.Text); - if (e.Direction == SpinDirection.Increase) - value++; - else - value--; + if (spinner.Content is TextBlock txtBox) + { + int value = Array.IndexOf(_mountains, txtBox.Text); + if (e.Direction == SpinDirection.Increase) + value++; + else + value--; - if (value < 0) - value = _mountains.Length - 1; - else if (value >= _mountains.Length) - value = 0; + if (value < 0) + value = _mountains.Length - 1; + else if (value >= _mountains.Length) + value = 0; + + txtBox.Text = _mountains[value]; + } - txtBox.Text = _mountains[value]; } private readonly string[] _mountains = new[] diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs index 3e748dd6f6..2d63f1fee9 100644 --- a/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs @@ -19,7 +19,7 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } - public void OnRepeatButtonClick(object sender, object args) + public void OnRepeatButtonClick(object? sender, object args) { repeatButtonClickCount++; var textBlock = this.Get("RepeatButtonTextBlock"); diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs index 5b74e3e19e..c6aab5c4d5 100644 --- a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs +++ b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs @@ -33,7 +33,7 @@ namespace ControlCatalog.Pages } - private void TransitionChanged(object sender, SelectionChangedEventArgs e) + private void TransitionChanged(object? sender, SelectionChangedEventArgs e) { switch (_transition.SelectedIndex) { diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs index eed46265ff..ef3d2bbafa 100644 --- a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs @@ -23,55 +23,79 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } - private async void CopyText(object sender, RoutedEventArgs args) + private async void CopyText(object? sender, RoutedEventArgs args) { - await Application.Current.Clipboard.SetTextAsync(ClipboardContent.Text); + if (Application.Current!.Clipboard is { } clipboard && ClipboardContent is { } clipboardContent) + await clipboard.SetTextAsync(clipboardContent.Text ?? String.Empty); } - private async void PasteText(object sender, RoutedEventArgs args) + private async void PasteText(object? sender, RoutedEventArgs args) { - ClipboardContent.Text = await Application.Current.Clipboard.GetTextAsync(); + if(Application.Current!.Clipboard is { } clipboard) + { + ClipboardContent.Text = await clipboard.GetTextAsync(); + } } - private async void CopyTextDataObject(object sender, RoutedEventArgs args) + private async void CopyTextDataObject(object? sender, RoutedEventArgs args) { - var dataObject = new DataObject(); - dataObject.Set(DataFormats.Text, ClipboardContent.Text ?? string.Empty); - await Application.Current.Clipboard.SetDataObjectAsync(dataObject); + if (Application.Current!.Clipboard is { } clipboard) + { + var dataObject = new DataObject(); + dataObject.Set(DataFormats.Text, ClipboardContent.Text ?? string.Empty); + await clipboard.SetDataObjectAsync(dataObject); + } } - private async void PasteTextDataObject(object sender, RoutedEventArgs args) + private async void PasteTextDataObject(object? sender, RoutedEventArgs args) { - ClipboardContent.Text = await Application.Current.Clipboard.GetDataAsync(DataFormats.Text) as string ?? string.Empty; + if (Application.Current!.Clipboard is { } clipboard) + { + ClipboardContent.Text = await clipboard.GetDataAsync(DataFormats.Text) as string ?? string.Empty; + } } - private async void CopyFilesDataObject(object sender, RoutedEventArgs args) + private async void CopyFilesDataObject(object? sender, RoutedEventArgs args) { - var files = ClipboardContent.Text.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); - if (files.Length == 0) + if (Application.Current!.Clipboard is { } clipboard) { - return; + var files = (ClipboardContent.Text ?? String.Empty) + .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + if (files.Length == 0) + { + return; + } + var dataObject = new DataObject(); + dataObject.Set(DataFormats.FileNames, files); + await clipboard.SetDataObjectAsync(dataObject); } - var dataObject = new DataObject(); - dataObject.Set(DataFormats.FileNames, files); - await Application.Current.Clipboard.SetDataObjectAsync(dataObject); } - private async void PasteFilesDataObject(object sender, RoutedEventArgs args) + private async void PasteFilesDataObject(object? sender, RoutedEventArgs args) { - var fiels = await Application.Current.Clipboard.GetDataAsync(DataFormats.FileNames) as IEnumerable; - ClipboardContent.Text = fiels != null ? string.Join(Environment.NewLine, fiels) : string.Empty; + if (Application.Current!.Clipboard is { } clipboard) + { + var fiels = await clipboard.GetDataAsync(DataFormats.FileNames) as IEnumerable; + ClipboardContent.Text = fiels != null ? string.Join(Environment.NewLine, fiels) : string.Empty; + } } private async void GetFormats(object sender, RoutedEventArgs args) { - var formats = await Application.Current.Clipboard.GetFormatsAsync(); - ClipboardContent.Text = string.Join(Environment.NewLine, formats); + if (Application.Current!.Clipboard is { } clipboard) + { + var formats = await clipboard.GetFormatsAsync(); + ClipboardContent.Text = string.Join(Environment.NewLine, formats); + } } private async void Clear(object sender, RoutedEventArgs args) { - await Application.Current.Clipboard.ClearAsync(); + if (Application.Current!.Clipboard is { } clipboard) + { + await clipboard.ClearAsync(); + } + } } } diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index d304bf227d..6d624c9a07 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -17,7 +17,7 @@ namespace ControlCatalog.Pages private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - var fontComboBox = this.Find("fontComboBox"); + var fontComboBox = this.Get("fontComboBox"); fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); fontComboBox.SelectedIndex = 0; } diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs index 18069ca857..61e0ed5acb 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -1,14 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Animations; @@ -18,7 +12,7 @@ namespace ControlCatalog.Pages; public partial class CompositionPage : UserControl { - private ImplicitAnimationCollection _implicitAnimations; + private ImplicitAnimationCollection? _implicitAnimations; public CompositionPage() { @@ -28,7 +22,7 @@ public partial class CompositionPage : UserControl protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - this.FindControl("Items").Items = CreateColorItems(); + this.Get("Items").Items = CreateColorItems(); } private List CreateColorItems() @@ -115,7 +109,6 @@ public partial class CompositionPage : UserControl public static void SetEnableAnimations(Border border, bool value) { - var page = border.FindAncestorOfType(); if (page == null) { @@ -127,8 +120,11 @@ public partial class CompositionPage : UserControl return; page.EnsureImplicitAnimations(); - ElementComposition.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = - page._implicitAnimations; + if (border.GetVisualParent() is Visual visualParent + && ElementComposition.GetElementVisual(visualParent) is CompositionVisual compositionVisual) + { + compositionVisual.ImplicitAnimations = page._implicitAnimations; + } } } @@ -150,4 +146,4 @@ public class CompositionPageColorItem { Color = color; } -} \ No newline at end of file +} diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs index 4d72fc5311..8bd1f4d85a 100644 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs @@ -52,13 +52,13 @@ namespace ControlCatalog.Pages base.OnDataContextChanged(e); } - private void ContextFlyoutPage_Closing(object sender, CancelEventArgs e) + private void ContextFlyoutPage_Closing(object? sender, CancelEventArgs e) { var cancelCloseCheckBox = this.FindControl("CancelCloseCheckBox"); e.Cancel = cancelCloseCheckBox?.IsChecked ?? false; } - private void ContextFlyoutPage_Opening(object sender, EventArgs e) + private void ContextFlyoutPage_Opening(object? sender, EventArgs e) { if (e is CancelEventArgs cancelArgs) { @@ -67,20 +67,20 @@ namespace ControlCatalog.Pages } } - private void CloseFlyout(object sender, RoutedEventArgs e) + private void CloseFlyout(object? sender, RoutedEventArgs e) { _textBox.ContextFlyout?.Hide(); } - public void CustomContextRequested(object sender, ContextRequestedEventArgs e) + public void CustomContextRequested(object? sender, ContextRequestedEventArgs e) { - var border = (Border)sender; - var textBlock = (TextBlock)border.Child; - - textBlock.Text = e.TryGetPosition(border, out var point) - ? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}" - : "Context was requested without pointer"; - e.Handled = true; + if (sender is Border border && border.Child is TextBlock textBlock) + { + textBlock.Text = e.TryGetPosition(border, out var point) + ? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}" + : "Context was requested without pointer"; + e.Handled = true; + } } private void InitializeComponent() diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs index 4581642024..96fcb54650 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs @@ -35,30 +35,31 @@ namespace ControlCatalog.Pages base.OnDataContextChanged(e); } - private void ContextFlyoutPage_Closing(object sender, CancelEventArgs e) + private void ContextFlyoutPage_Closing(object? sender, CancelEventArgs e) { var cancelCloseCheckBox = this.FindControl("CancelCloseCheckBox"); - e.Cancel = cancelCloseCheckBox.IsChecked ?? false; + e.Cancel = cancelCloseCheckBox?.IsChecked ?? false; } - private void ContextFlyoutPage_Opening(object sender, EventArgs e) + private void ContextFlyoutPage_Opening(object? sender, EventArgs e) { if (e is CancelEventArgs cancelArgs) { var cancelCloseCheckBox = this.FindControl("CancelOpenCheckBox"); - cancelArgs.Cancel = cancelCloseCheckBox.IsChecked ?? false; + cancelArgs.Cancel = cancelCloseCheckBox?.IsChecked ?? false; } } - public void CustomContextRequested(object sender, ContextRequestedEventArgs e) + public void CustomContextRequested(object? sender, ContextRequestedEventArgs e) { - var border = (Border)sender; - var textBlock = (TextBlock)border.Child; + if (sender is Border border && border.Child is TextBlock textBlock) + { + textBlock.Text = e.TryGetPosition(border, out var point) + ? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}" + : "Context was requested without pointer"; + e.Handled = true; + } - textBlock.Text = e.TryGetPosition(border, out var point) - ? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}" - : "Context was requested without pointer"; - e.Handled = true; } private void InitializeComponent() diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs index 219b7aeac4..3565d113bc 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs @@ -62,7 +62,7 @@ namespace ControlCatalog.Pages addButton.Click += (a, b) => collectionView3.AddNew(); } - private void Dg1_LoadingRow(object sender, DataGridRowEventArgs e) + private void Dg1_LoadingRow(object? sender, DataGridRowEventArgs e) { e.Row.Header = e.Row.GetIndex() + 1; } @@ -74,7 +74,7 @@ namespace ControlCatalog.Pages private class ReversedStringComparer : IComparer, IComparer { - public int Compare(object x, object y) + public int Compare(object? x, object? y) { if (x is string left && y is string right) { diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index f7b6db1255..67e9ef4e40 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -111,9 +111,16 @@ namespace ControlCatalog.Pages Title = "Select folder", Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null }.ShowAsync(GetWindow()); - lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result)); - results.Items = new [] { result }; - resultsVisible.IsVisible = result != null; + if (string.IsNullOrEmpty(result)) + { + resultsVisible.IsVisible = false; + } + else + { + lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result)); + results.Items = new[] { result }; + resultsVisible.IsVisible = true; + } }; this.Get diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index 001f186761..43875dd990 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -1,21 +1,32 @@ using System; +using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; -using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using Avalonia.Rendering; +using Avalonia.Threading; namespace IntegrationTestApp { public class ShowWindowTest : Window { + private readonly DispatcherTimer? _timer; + private readonly TextBox? _orderTextBox; + public ShowWindowTest() { InitializeComponent(); DataContext = this; PositionChanged += (s, e) => this.GetControl("Position").Text = $"{Position}"; - } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + _orderTextBox = this.GetControl("Order"); + _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; + _timer.Tick += TimerOnTick; + _timer.Start(); + } + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); @@ -36,5 +47,16 @@ namespace IntegrationTestApp ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}"; } } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + _timer?.Stop(); + } + + private void TimerOnTick(object? sender, EventArgs e) + { + _orderTextBox!.Text = MacOSIntegration.GetOrderedIndex(this).ToString(); + } } } diff --git a/samples/PlatformSanityChecks/App.xaml b/samples/PlatformSanityChecks/App.xaml index 25bab6ae35..1b9d64fca6 100644 --- a/samples/PlatformSanityChecks/App.xaml +++ b/samples/PlatformSanityChecks/App.xaml @@ -1,6 +1,5 @@ - - + diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 4f7f06b529..5c743aabdb 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -7,7 +7,7 @@ - + diff --git a/samples/Previewer/App.xaml b/samples/Previewer/App.xaml index 6bae1955af..1b9d64fca6 100644 --- a/samples/Previewer/App.xaml +++ b/samples/Previewer/App.xaml @@ -1,6 +1,5 @@ - - + - \ No newline at end of file + diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index 98560e9ab0..2cc84168dc 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -10,7 +10,7 @@ - + diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index 9c524a7932..bf27747154 100644 --- a/samples/RenderDemo/Pages/CustomSkiaPage.cs +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -40,14 +40,16 @@ namespace RenderDemo.Pages static Stopwatch St = Stopwatch.StartNew(); public void Render(IDrawingContextImpl context) { - var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas; - if (canvas == null) + var leaseFeature = context.GetFeature(); + if (leaseFeature == null) using (var c = new DrawingContext(context, false)) { c.DrawText(_noSkia, new Point()); } else { + using var lease = leaseFeature.Lease(); + var canvas = lease.SkCanvas; canvas.Save(); // create the first shader var colors = new SKColor[] { diff --git a/samples/RenderDemo/Pages/DrawingPage.xaml b/samples/RenderDemo/Pages/DrawingPage.xaml index 7bdadf586b..a658dbc089 100644 --- a/samples/RenderDemo/Pages/DrawingPage.xaml +++ b/samples/RenderDemo/Pages/DrawingPage.xaml @@ -1,134 +1,141 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - + + + + - - \ No newline at end of file + + diff --git a/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs b/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs index 92eb2e7dec..57a5c7101f 100644 --- a/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs +++ b/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs @@ -78,7 +78,7 @@ namespace RenderDemo.Pages _defaultProperties = defaultProperties; } - public TextRun? GetTextRun(int textSourceIndex) + public TextRun GetTextRun(int textSourceIndex) { if (textSourceIndex >= _text.Length * 2 + TextRun.DefaultTextSourceLength) { @@ -107,7 +107,7 @@ namespace RenderDemo.Pages public Control Control => _control; public override Size Size => _control.DesiredSize; public override double Baseline => 0; - public override TextRunProperties? Properties { get; } + public override TextRunProperties Properties { get; } public override void Draw(DrawingContext drawingContext, Point origin) { diff --git a/samples/VirtualizationDemo/App.xaml b/samples/VirtualizationDemo/App.xaml index 3ad1dce794..eb5f0e4dca 100644 --- a/samples/VirtualizationDemo/App.xaml +++ b/samples/VirtualizationDemo/App.xaml @@ -1,9 +1,7 @@ - - - - - - \ No newline at end of file + + + + + diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index bd6054327f..b27cfe77e8 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -5,7 +5,7 @@ - + diff --git a/samples/interop/Direct3DInteropSample/App.paml b/samples/interop/Direct3DInteropSample/App.paml index d9630eef58..e6d77dfaf4 100644 --- a/samples/interop/Direct3DInteropSample/App.paml +++ b/samples/interop/Direct3DInteropSample/App.paml @@ -1,6 +1,5 @@ - - + - \ No newline at end of file + diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj index 81b94b4d09..f9ef4693d5 100644 --- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj +++ b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 0c72c389dc..ed5b46d398 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -8,7 +8,8 @@ using Avalonia.Input.Platform; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; -using Avalonia.Skia; +using Avalonia.Rendering.Composition; +using Avalonia.OpenGL; namespace Avalonia { @@ -16,10 +17,8 @@ namespace Avalonia { public static T UseAndroid(this T builder) where T : AppBuilderBase, new() { - var options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions(); - return builder - .UseWindowingSubsystem(() => AndroidPlatform.Initialize(options), "Android") + .UseWindowingSubsystem(() => AndroidPlatform.Initialize(), "Android") .UseSkia(); } } @@ -42,9 +41,11 @@ namespace Avalonia.Android public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); - public static void Initialize(AndroidPlatformOptions options) + internal static Compositor Compositor { get; private set; } + + public static void Initialize() { - Options = options; + Options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions(); AvaloniaLocator.CurrentMutable .Bind().ToTransient() @@ -58,16 +59,24 @@ namespace Avalonia.Android .Bind().ToConstant(new RenderLoop()) .Bind().ToSingleton(); - if (options.UseGpu) + if (Options.UseGpu) { EglPlatformOpenGlInterface.TryInitialize(); } + + if (Options.UseCompositor) + { + Compositor = new Compositor( + AvaloniaLocator.Current.GetRequiredService(), + AvaloniaLocator.Current.GetService()); + } } } public sealed class AndroidPlatformOptions { - public bool UseDeferredRendering { get; set; } = true; + public bool UseDeferredRendering { get; set; } = false; public bool UseGpu { get; set; } = true; + public bool UseCompositor { get; set; } = true; } } diff --git a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs index e72f0aed90..de9149e9a1 100644 --- a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs +++ b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs @@ -14,6 +14,7 @@ namespace Avalonia.Android internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface { private Handler _handler; + private static Thread s_uiThread; public AndroidThreadingInterface() { @@ -26,46 +27,33 @@ namespace Avalonia.Android { if (interval.TotalMilliseconds < 10) interval = TimeSpan.FromMilliseconds(10); - object l = new object(); + var stopped = false; Timer timer = null; - var scheduled = false; timer = new Timer(_ => { - lock (l) + if (stopped) + return; + + EnsureInvokeOnMainThread(() => { - if (stopped) + try { - timer.Dispose(); - return; + tick(); } - if (scheduled) - return; - scheduled = true; - EnsureInvokeOnMainThread(() => + finally { - try - { - tick(); - } - finally - { - lock (l) - { - scheduled = false; - } - } - }); - } - }, null, TimeSpan.Zero, interval); + if (!stopped) + timer.Change(interval, Timeout.InfiniteTimeSpan); + } + }); + }, + null, interval, Timeout.InfiniteTimeSpan); return Disposable.Create(() => { - lock (l) - { - stopped = true; - timer.Dispose(); - } + stopped = true; + timer.Dispose(); }); } @@ -76,7 +64,25 @@ namespace Avalonia.Android EnsureInvokeOnMainThread(() => Signaled?.Invoke(null)); } - public bool CurrentThreadIsLoopThread => Looper.MainLooper.Thread.Equals(Java.Lang.Thread.CurrentThread()); + public bool CurrentThreadIsLoopThread + { + get + { + if (s_uiThread != null) + return s_uiThread == Thread.CurrentThread; + + var isOnMainThread = OperatingSystem.IsAndroidVersionAtLeast(23) + ? Looper.MainLooper.IsCurrentThread + : Looper.MainLooper.Thread.Equals(Java.Lang.Thread.CurrentThread()); + if (isOnMainThread) + { + s_uiThread = Thread.CurrentThread; + return true; + } + + return false; + } + } public event Action Signaled; } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 5343b57251..33501ece06 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -47,18 +47,6 @@ namespace Avalonia.Android } } - [Obsolete("deprecated")] - public override void Invalidate(global::Android.Graphics.Rect dirty) - { - Invalidate(); - } - - [Obsolete("deprecated")] - public override void Invalidate(int l, int t, int r, int b) - { - Invalidate(); - } - public void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) { Log.Info("AVALONIA", "Surface Changed"); diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index fd97e293f9..17a0d6b63a 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; + using Android.Content; using Android.Graphics; using Android.Views; @@ -19,6 +20,7 @@ using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; namespace Avalonia.Android.Platform.SkiaPlatform { @@ -29,7 +31,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly IFramebufferPlatformSurface _framebuffer; private readonly AndroidKeyboardEventsHelper _keyboardHelper; - private readonly AndroidTouchEventsHelper _touchHelper; + private readonly AndroidMotionEventsHelper _pointerHelper; private readonly ITextInputMethodImpl _textInputMethod; private ViewImpl _view; @@ -38,8 +40,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view = new ViewImpl(avaloniaView.Context, this, placeOnTop); _textInputMethod = new AndroidInputMethod(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); - _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, - GetAvaloniaPointFromEvent); + _pointerHelper = new AndroidMotionEventsHelper(this); _gl = GlPlatformSurface.TryCreate(this); _framebuffer = new FramebufferManager(this); @@ -84,9 +85,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IEnumerable Surfaces => new object[] { _gl, _framebuffer, Handle }; public IRenderer CreateRenderer(IRenderRoot root) => - AndroidPlatform.Options.UseDeferredRendering - ? new DeferredRenderer(root, AvaloniaLocator.Current.GetService()) { RenderOnlyOnRenderThread = true } - : new ImmediateRenderer(root); + AndroidPlatform.Options.UseCompositor + ? new CompositingRenderer(root, AndroidPlatform.Compositor) + : AndroidPlatform.Options.UseDeferredRendering + ? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService()) { RenderOnlyOnRenderThread = true } + : new ImmediateRenderer(root); public virtual void Hide() { @@ -157,10 +160,19 @@ namespace Avalonia.Android.Platform.SkiaPlatform _tl.Draw(); } + protected override bool DispatchGenericPointerEvent(MotionEvent e) + { + bool callBase; + bool? result = _tl._pointerHelper.DispatchMotionEvent(e, out callBase); + bool baseResult = callBase ? base.DispatchGenericPointerEvent(e) : false; + + return result != null ? result.Value : baseResult; + } + public override bool DispatchTouchEvent(MotionEvent e) { bool callBase; - bool? result = _tl._touchHelper.DispatchTouchEvent(e, out callBase); + bool? result = _tl._pointerHelper.DispatchMotionEvent(e, out callBase); bool baseResult = callBase ? base.DispatchTouchEvent(e) : false; return result != null ? result.Value : baseResult; diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs new file mode 100644 index 0000000000..ce385ebe34 --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; + +using Android.Views; + +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Collections.Pooled; +using Avalonia.Input; +using Avalonia.Input.Raw; + +#nullable enable + +namespace Avalonia.Android.Platform.Specific.Helpers +{ + internal class AndroidMotionEventsHelper : IDisposable + { + private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never); + private static readonly float s_radiansToDegree = (float)(180f * Math.PI); + private readonly TouchDevice _touchDevice; + private readonly MouseDevice _mouseDevice; + private readonly PenDevice _penDevice; + private readonly TopLevelImpl _view; + private bool _disposed; + + public AndroidMotionEventsHelper(TopLevelImpl view) + { + _touchDevice = new TouchDevice(); + _penDevice = new PenDevice(); + _mouseDevice = new MouseDevice(); + _view = view; + } + + public bool? DispatchMotionEvent(MotionEvent e, out bool callBase) + { + callBase = true; + if (_disposed) + { + return null; + } + + var eventTime = (ulong)DateTime.Now.Millisecond; + var inputRoot = _view.InputRoot; + var actionMasked = e.ActionMasked; + var modifiers = GetModifiers(e.MetaState, e.ButtonState); + + if (actionMasked == MotionEventActions.Move) + { + for (int index = 0; index < e.PointerCount; index++) + { + var toolType = e.GetToolType(index); + var device = GetDevice(toolType); + var eventType = toolType == MotionEventToolType.Finger ? RawPointerEventType.TouchUpdate : RawPointerEventType.Move; + var point = CreatePoint(e, index); + modifiers |= GetToolModifiers(toolType); + + // ButtonState reports only mouse buttons, but not touch or stylus pointer. + if (toolType != MotionEventToolType.Mouse) + { + modifiers |= RawInputModifiers.LeftMouseButton; + } + + var args = new RawTouchEventArgs(device, eventTime, inputRoot, eventType, point, modifiers, e.GetPointerId(index)) + { + IntermediatePoints = new Lazy?>(() => + { + var site = e.HistorySize; + s_intermediatePointsPooledList.Clear(); + s_intermediatePointsPooledList.Capacity = site; + + for (int pos = 0; pos < site; pos++) + { + s_intermediatePointsPooledList.Add(CreateHistoricalPoint(e, index, pos)); + } + + return s_intermediatePointsPooledList; + }) + }; + _view.Input(args); + } + } + else + { + var index = e.ActionIndex; + var toolType = e.GetToolType(index); + var device = GetDevice(toolType); + modifiers |= GetToolModifiers(toolType); + var point = CreatePoint(e, index); + + if (actionMasked == MotionEventActions.Scroll && toolType == MotionEventToolType.Mouse) + { + var delta = new Vector(e.GetAxisValue(Axis.Hscroll), e.GetAxisValue(Axis.Vscroll)); + var args = new RawMouseWheelEventArgs(device, eventTime, inputRoot, point.Position, delta, RawInputModifiers.None); + _view.Input(args); + } + else + { + var eventType = GetActionType(e, actionMasked, toolType); + if (eventType >= 0) + { + var args = new RawTouchEventArgs(device, eventTime, inputRoot, eventType, point, modifiers, e.GetPointerId(index)); + _view.Input(args); + } + } + } + + return true; + } + + private static RawInputModifiers GetModifiers(MetaKeyStates metaState, MotionEventButtonState buttonState) + { + var modifiers = RawInputModifiers.None; + if (metaState.HasAnyFlag(MetaKeyStates.ShiftOn)) + { + modifiers |= RawInputModifiers.Shift; + } + if (metaState.HasAnyFlag(MetaKeyStates.CtrlOn)) + { + modifiers |= RawInputModifiers.Control; + } + if (metaState.HasAnyFlag(MetaKeyStates.AltOn)) + { + modifiers |= RawInputModifiers.Alt; + } + if (metaState.HasAnyFlag(MetaKeyStates.MetaOn)) + { + modifiers |= RawInputModifiers.Meta; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.Primary)) + { + modifiers |= RawInputModifiers.LeftMouseButton; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.Secondary)) + { + modifiers |= RawInputModifiers.RightMouseButton; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.Tertiary)) + { + modifiers |= RawInputModifiers.MiddleMouseButton; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.Back)) + { + modifiers |= RawInputModifiers.XButton1MouseButton; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.Forward)) + { + modifiers |= RawInputModifiers.XButton2MouseButton; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.StylusPrimary)) + { + modifiers |= RawInputModifiers.PenBarrelButton; + } + return modifiers; + } + +#pragma warning disable CA1416 // Validate platform compatibility + private static RawPointerEventType GetActionType(MotionEvent e, MotionEventActions actionMasked, MotionEventToolType toolType) + { + var isTouch = toolType == MotionEventToolType.Finger; + var isMouse = toolType == MotionEventToolType.Mouse; + switch (actionMasked) + { + // DOWN + case MotionEventActions.Down when !isMouse: + case MotionEventActions.PointerDown when !isMouse: + return isTouch ? RawPointerEventType.TouchBegin : RawPointerEventType.LeftButtonDown; + case MotionEventActions.ButtonPress: + return e.ActionButton switch + { + MotionEventButtonState.Back => RawPointerEventType.XButton1Down, + MotionEventButtonState.Forward => RawPointerEventType.XButton2Down, + MotionEventButtonState.Primary => RawPointerEventType.LeftButtonDown, + MotionEventButtonState.Secondary => RawPointerEventType.RightButtonDown, + MotionEventButtonState.StylusPrimary => RawPointerEventType.LeftButtonDown, + MotionEventButtonState.StylusSecondary => RawPointerEventType.RightButtonDown, + MotionEventButtonState.Tertiary => RawPointerEventType.MiddleButtonDown, + _ => RawPointerEventType.LeftButtonDown + }; + // UP + case MotionEventActions.Up when !isMouse: + case MotionEventActions.PointerUp when !isMouse: + return isTouch ? RawPointerEventType.TouchEnd : RawPointerEventType.LeftButtonUp; + case MotionEventActions.ButtonRelease: + return e.ActionButton switch + { + MotionEventButtonState.Back => RawPointerEventType.XButton1Up, + MotionEventButtonState.Forward => RawPointerEventType.XButton2Up, + MotionEventButtonState.Primary => RawPointerEventType.LeftButtonUp, + MotionEventButtonState.Secondary => RawPointerEventType.RightButtonUp, + MotionEventButtonState.StylusPrimary => RawPointerEventType.LeftButtonUp, + MotionEventButtonState.StylusSecondary => RawPointerEventType.RightButtonUp, + MotionEventButtonState.Tertiary => RawPointerEventType.MiddleButtonUp, + _ => RawPointerEventType.LeftButtonUp + }; + // MOVE + case MotionEventActions.Outside: + case MotionEventActions.HoverMove: + case MotionEventActions.Move: + return isTouch ? RawPointerEventType.TouchUpdate : RawPointerEventType.Move; + // CANCEL + case MotionEventActions.Cancel: + return isTouch ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow; + default: + return (RawPointerEventType)(-1); + } + } +#pragma warning restore CA1416 // Validate platform compatibility + + private IPointerDevice GetDevice(MotionEventToolType type) + { + return type switch + { + MotionEventToolType.Mouse => _mouseDevice, + MotionEventToolType.Stylus => _penDevice, + MotionEventToolType.Eraser => _penDevice, + MotionEventToolType.Finger => _touchDevice, + _ => _touchDevice + }; + } + + private RawPointerPoint CreatePoint(MotionEvent e, int index) + { + return new RawPointerPoint + { + Position = new Point(e.GetX(index), e.GetY(index)) / _view.RenderScaling, + Pressure = Math.Min(e.GetPressure(index), 1), // android pressure can depend on the device, can be mixed up with "GetSize", may be larger than 1.0f on some devices + Twist = e.GetOrientation(index) * s_radiansToDegree + }; + } + + private RawPointerPoint CreateHistoricalPoint(MotionEvent e, int index, int pos) + { + return new RawPointerPoint + { + Position = new Point(e.GetHistoricalX(index, pos), e.GetHistoricalY(index, pos)) / _view.RenderScaling, + Pressure = Math.Min(e.GetHistoricalPressure(index, pos), 1), + Twist = e.GetHistoricalOrientation(index, pos) * s_radiansToDegree + }; + } + + private static RawInputModifiers GetToolModifiers(MotionEventToolType toolType) + { + // Android "Eraser" indicates Inverted pen OR actual Eraser. So we have to go both here. + return toolType == MotionEventToolType.Eraser ? RawInputModifiers.PenInverted | RawInputModifiers.PenEraser : RawInputModifiers.None; + } + + public void Dispose() + { + _disposed = true; + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs deleted file mode 100644 index 6142598514..0000000000 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using Android.Views; -using Avalonia.Input; -using Avalonia.Input.Raw; -using Avalonia.Platform; - -namespace Avalonia.Android.Platform.Specific.Helpers -{ - public class AndroidTouchEventsHelper : IDisposable where TView : ITopLevelImpl, IAndroidView - { - private TView _view; - public bool HandleEvents { get; set; } - - public AndroidTouchEventsHelper(TView view, Func getInputRoot, Func getPointfunc) - { - this._view = view; - HandleEvents = true; - _getPointFunc = getPointfunc; - _getInputRoot = getInputRoot; - } - - private TouchDevice _touchDevice = new TouchDevice(); - private Func _getPointFunc; - private Func _getInputRoot; - - public bool? DispatchTouchEvent(MotionEvent e, out bool callBase) - { - if (!HandleEvents) - { - callBase = true; - return null; - } - - var eventTime = DateTime.Now; - - //Basic touch support - var pointerEventType = e.Action switch - { - MotionEventActions.Down => RawPointerEventType.TouchBegin, - MotionEventActions.Up => RawPointerEventType.TouchEnd, - MotionEventActions.Cancel => RawPointerEventType.TouchCancel, - _ => RawPointerEventType.TouchUpdate - }; - - if (e.Action.HasFlag(MotionEventActions.PointerDown)) - { - pointerEventType = RawPointerEventType.TouchBegin; - } - - if (e.Action.HasFlag(MotionEventActions.PointerUp)) - { - pointerEventType = RawPointerEventType.TouchEnd; - } - - for (int i = 0; i < e.PointerCount; i++) - { - //if point is in view otherwise it's possible avalonia not to find the proper window to dispatch the event - var point = _getPointFunc(e, i); - - double x = _view.View.GetX(); - double y = _view.View.GetY(); - double r = x + _view.View.Width; - double b = y + _view.View.Height; - - if (x <= point.X && r >= point.X && y <= point.Y && b >= point.Y) - { - var inputRoot = _getInputRoot(); - - var mouseEvent = new RawTouchEventArgs(_touchDevice, (uint)eventTime.Ticks, inputRoot, - i == e.ActionIndex ? pointerEventType : RawPointerEventType.TouchUpdate, point, RawInputModifiers.None, e.GetPointerId(i)); - _view.Input(mouseEvent); - } - } - - callBase = true; - //if return false events for move and up are not received!!! - return e.Action != MotionEventActions.Up; - } - - public void Dispose() - { - HandleEvents = false; - } - } -} diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 50581d47b1..a9b2e16d43 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -35,13 +36,13 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem public bool CanBookmark => true; - public Task SaveBookmark() + public Task SaveBookmarkAsync() { Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); return Task.FromResult(Uri.ToString()); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); return Task.CompletedTask; @@ -106,6 +107,30 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar { return Task.FromResult(new StorageItemProperties()); } + + public async Task> GetItemsAsync() + { + using var javaFile = new JavaFile(Uri.Path!); + + // Java file represents files AND directories. Don't be confused. + var files = await javaFile.ListFilesAsync().ConfigureAwait(false); + if (files is null) + { + return Array.Empty(); + } + + return files + .Select(f => (file: f, uri: AndroidUri.FromFile(f))) + .Where(t => t.uri is not null) + .Select(t => t.file switch + { + { IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!), + { IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!), + _ => null + }) + .Where(i => i is not null) + .ToArray()!; + } } internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile @@ -118,10 +143,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF public bool CanOpenWrite => true; - public Task OpenRead() => Task.FromResult(OpenContentStream(Context, Uri, false) + public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false) ?? throw new InvalidOperationException("Failed to open content stream")); - public Task OpenWrite() => Task.FromResult(OpenContentStream(Context, Uri, true) + public Task OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true) ?? throw new InvalidOperationException("Failed to open content stream")); private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput) diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs index 03b2d17e44..e0883901fd 100644 --- a/src/Avalonia.Base/Animation/Animation.cs +++ b/src/Avalonia.Base/Animation/Animation.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -172,23 +173,6 @@ namespace Avalonia.Animation set { SetAndRaise(SpeedRatioProperty, ref _speedRatio, value); } } - /// - /// Obsolete: Do not use this property, use instead. - /// - /// - [Obsolete("This property has been superceded by IterationCount.")] - public string RepeatCount - { - get { return IterationCount.ToString(); } - set - { - var val = value.ToUpper(); - val = val.Replace("LOOP", "INFINITE"); - val = val.Replace("NONE", "1"); - IterationCount = IterationCount.Parse(val); - } - } - /// /// Gets the children of the . /// @@ -196,14 +180,14 @@ namespace Avalonia.Animation public KeyFrames Children { get; } = new KeyFrames(); // Store values for the Animator attached properties for IAnimationSetter objects. - private static readonly Dictionary s_animators = new Dictionary(); + private static readonly Dictionary Factory)> s_animators = new(); /// /// Gets the value of the Animator attached property for a setter. /// /// The animation setter. /// The property animator type. - public static Type? GetAnimator(IAnimationSetter setter) + public static (Type Type, Func Factory)? GetAnimator(IAnimationSetter setter) { if (s_animators.TryGetValue(setter, out var type)) { @@ -217,24 +201,28 @@ namespace Avalonia.Animation /// /// The animation setter. /// The property animator value. - public static void SetAnimator(IAnimationSetter setter, Type value) + public static void SetAnimator(IAnimationSetter setter, +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicMethods)] +#endif + Type value) { - s_animators[setter] = value; + s_animators[setter] = (value, () => (IAnimator)Activator.CreateInstance(value)!); } - private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> + private readonly static List<(Func Condition, Type Animator, Func Factory)> Animators = new() { - ( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ), - ( prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator) ), - ( prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator) ), - ( prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator) ), - ( prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator) ), - ( prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator) ), - ( prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator) ), - ( prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator) ), - ( prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator) ), - ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ), - ( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator) ), + ( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator), () => new BoolAnimator() ), + ( prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator), () => new ByteAnimator() ), + ( prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator), () => new Int16Animator() ), + ( prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator), () => new Int32Animator() ), + ( prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator), () => new Int64Animator() ), + ( prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator), () => new UInt16Animator() ), + ( prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator), () => new UInt32Animator() ), + ( prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator), () => new UInt64Animator() ), + ( prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator), () => new FloatAnimator() ), + ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator), () => new DoubleAnimator() ), + ( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator), () => new DecimalAnimator() ), }; /// @@ -249,18 +237,18 @@ namespace Avalonia.Animation /// The type of the animator to instantiate. /// public static void RegisterAnimator(Func condition) - where TAnimator : IAnimator + where TAnimator : IAnimator, new() { - Animators.Insert(0, (condition, typeof(TAnimator))); + Animators.Insert(0, (condition, typeof(TAnimator), () => new TAnimator())); } - private static Type? GetAnimatorType(AvaloniaProperty property) + private static (Type Type, Func Factory)? GetAnimatorType(AvaloniaProperty property) { - foreach (var (condition, type) in Animators) + foreach (var (condition, type, factory) in Animators) { if (condition(property)) { - return type; + return (type, factory); } } return null; @@ -268,7 +256,7 @@ namespace Avalonia.Animation private (IList Animators, IList subscriptions) InterpretKeyframes(Animatable control) { - var handlerList = new List<(Type type, AvaloniaProperty property)>(); + var handlerList = new Dictionary<(Type type, AvaloniaProperty Property), Func>(); var animatorKeyFrames = new List(); var subscriptions = new List(); @@ -288,8 +276,10 @@ namespace Avalonia.Animation throw new InvalidOperationException($"No animator registered for the property {setter.Property}. Add an animator to the Animation.Animators collection that matches this property to animate it."); } - if (!handlerList.Contains((handler, setter.Property))) - handlerList.Add((handler, setter.Property)); + var (type, factory) = handler.Value; + + if (!handlerList.ContainsKey((type, setter.Property))) + handlerList[(type, setter.Property)] = factory; var cue = keyframe.Cue; @@ -298,7 +288,7 @@ namespace Avalonia.Animation cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds); } - var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline); + var newKF = new AnimatorKeyFrame(type, factory, cue, keyframe.KeySpline); subscriptions.Add(newKF.BindSetter(setter, control)); @@ -308,10 +298,10 @@ namespace Avalonia.Animation var newAnimatorInstances = new List(); - foreach (var (handlerType, property) in handlerList) + foreach (var handler in handlerList) { - var newInstance = (IAnimator)Activator.CreateInstance(handlerType)!; - newInstance.Property = property; + var newInstance = handler.Value(); + newInstance.Property = handler.Key.Property; newAnimatorInstances.Add(newInstance); } diff --git a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs index 8af31f2948..0356723f00 100644 --- a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs @@ -20,22 +20,25 @@ namespace Avalonia.Animation } - public AnimatorKeyFrame(Type? animatorType, Cue cue) + public AnimatorKeyFrame(Type? animatorType, Func? animatorFactory, Cue cue) { AnimatorType = animatorType; + AnimatorFactory = animatorFactory; Cue = cue; KeySpline = null; } - public AnimatorKeyFrame(Type? animatorType, Cue cue, KeySpline? keySpline) + public AnimatorKeyFrame(Type? animatorType, Func? animatorFactory, Cue cue, KeySpline? keySpline) { AnimatorType = animatorType; + AnimatorFactory = animatorFactory; Cue = cue; KeySpline = keySpline; } internal bool isNeutral; public Type? AnimatorType { get; } + public Func? AnimatorFactory { get; } public Cue Cue { get; } public KeySpline? KeySpline { get; } public AvaloniaProperty? Property { get; private set; } diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs index 248ca61c1d..8765cfb4c9 100644 --- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Base/Animation/Animators/Animator`1.cs @@ -171,12 +171,12 @@ namespace Avalonia.Animation.Animators { if (!hasStartKey) { - _convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true }); + _convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, null, new Cue(0.0d)) { Value = default(T), isNeutral = true }); } if (!hasEndKey) { - _convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(1.0d)) { Value = default(T), isNeutral = true }); + _convertedKeyframes.Add(new AnimatorKeyFrame(null, null, new Cue(1.0d)) { Value = default(T), isNeutral = true }); } } } diff --git a/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs index 5f22254fb5..07c147ef6d 100644 --- a/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs +++ b/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs @@ -17,8 +17,7 @@ namespace Avalonia.Animation.Animators /// public class BaseBrushAnimator : Animator { - private static readonly List<(Func Match, Type AnimatorType)> _brushAnimators = - new List<(Func Match, Type AnimatorType)>(); + private static readonly List<(Func Match, Type AnimatorType, Func AnimatorFactory)> _brushAnimators = new(); /// /// Register an that handles a specific @@ -34,7 +33,7 @@ namespace Avalonia.Animation.Animators public static void RegisterBrushAnimator(Func condition) where TAnimator : IAnimator, new() { - _brushAnimators.Insert(0, (condition, typeof(TAnimator))); + _brushAnimators.Insert(0, (condition, typeof(TAnimator), () => new TAnimator())); } /// @@ -85,14 +84,14 @@ namespace Avalonia.Animation.Animators { if (keyframe.Value is ISolidColorBrush solidColorBrush) { - gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline) + gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), () => new GradientBrushAnimator(), keyframe.Cue, keyframe.KeySpline) { Value = GradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush) }); } else if (keyframe.Value is IGradientBrush) { - gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline) + gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), () => new GradientBrushAnimator(), keyframe.Cue, keyframe.KeySpline) { Value = keyframe.Value }); @@ -117,7 +116,7 @@ namespace Avalonia.Animation.Animators { if (keyframe.Value is ISolidColorBrush) { - solidColorBrushAnimator.Add(new AnimatorKeyFrame(typeof(ISolidColorBrushAnimator), keyframe.Cue, keyframe.KeySpline) + solidColorBrushAnimator.Add(new AnimatorKeyFrame(typeof(ISolidColorBrushAnimator), () => new ISolidColorBrushAnimator(), keyframe.Cue, keyframe.KeySpline) { Value = keyframe.Value }); @@ -137,18 +136,18 @@ namespace Avalonia.Animation.Animators { if (_brushAnimators.Count > 0 && this[0].Value?.GetType() is Type firstKeyType) { - foreach (var (match, animatorType) in _brushAnimators) + foreach (var (match, animatorType, animatorFactory) in _brushAnimators) { if (!match(firstKeyType)) continue; - animator = (IAnimator?)Activator.CreateInstance(animatorType); + animator = animatorFactory(); if (animator != null) { animator.Property = Property; foreach (var keyframe in this) { - animator.Add(new AnimatorKeyFrame(animatorType, keyframe.Cue, keyframe.KeySpline) + animator.Add(new AnimatorKeyFrame(animatorType, animatorFactory, keyframe.Cue, keyframe.KeySpline) { Value = keyframe.Value }); diff --git a/src/Avalonia.Base/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/SolidColorBrushAnimator.cs index b87b2681d6..9256c7be5e 100644 --- a/src/Avalonia.Base/Animation/Animators/SolidColorBrushAnimator.cs +++ b/src/Avalonia.Base/Animation/Animators/SolidColorBrushAnimator.cs @@ -37,17 +37,4 @@ namespace Avalonia.Animation.Animators } } - [Obsolete("Use ISolidColorBrushAnimator instead")] - public class SolidColorBrushAnimator : Animator - { - public override SolidColorBrush? Interpolate(double progress, SolidColorBrush? oldValue, SolidColorBrush? newValue) - { - if (oldValue is null || newValue is null) - { - return progress >= 0.5 ? newValue : oldValue; - } - - return new SolidColorBrush(ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color)); - } - } } diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 15feed388b..d091b9072d 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -6,6 +6,7 @@ True true $(BaseIntermediateOutputPath)\GeneratedFiles + true diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 134e3b2ac7..2d7bab6cd6 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -468,41 +468,6 @@ namespace Avalonia }); } - /// - /// Subscribes to a property changed notifications for changes that originate from a - /// . - /// - /// The type of the property change sender. - /// The property changed observable. - /// Given a TTarget, returns the handler. - /// A disposable that can be used to terminate the subscription. - [Obsolete("Use overload taking Action.")] - public static IDisposable AddClassHandler( - this IObservable observable, - Func> handler) - where TTarget : class - { - return observable.Subscribe(e => SubscribeAdapter(e, handler)); - } - - /// - /// Observer method for . - /// - /// The sender type to accept. - /// The event args. - /// Given a TTarget, returns the handler. - private static void SubscribeAdapter( - AvaloniaPropertyChangedEventArgs e, - Func> handler) - where TTarget : class - { - if (e.Sender is TTarget target) - { - handler(target)(e); - } - } - private class BindingAdaptor : IBinding { private IObservable _source; diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index 3937ee5658..14acc2c47f 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/src/Avalonia.Base/AvaloniaProperty`1.cs @@ -30,21 +30,6 @@ namespace Avalonia _changed = new Subject>(); } - /// - /// Initializes a new instance of the class. - /// - /// The property to copy. - /// The new owner type. - /// Optional overridden metadata. - [Obsolete("Use constructor with AvaloniaProperty instead.", true)] - protected AvaloniaProperty( - AvaloniaProperty source, - Type ownerType, - AvaloniaPropertyMetadata? metadata) - : this(source as AvaloniaProperty ?? throw new InvalidOperationException(), ownerType, metadata) - { - } - /// /// Initializes a new instance of the class. /// diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index fb70729cf8..c4684960d6 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs @@ -140,21 +140,6 @@ namespace Avalonia.Collections } } - [Obsolete("Causes memory leaks. Use DynamicData or similar instead.")] - public static IAvaloniaReadOnlyList CreateDerivedList( - this IAvaloniaReadOnlyList collection, - Func select) - { - var result = new AvaloniaList(); - - collection.ForEachItem( - (i, item) => result.Insert(i, select(item)), - (i, item) => result.RemoveAt(i), - () => result.Clear()); - - return result; - } - /// /// Listens for property changed events from all items in a collection. /// diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 77863e5101..d6197c50c6 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using Avalonia.Collections; +using Avalonia.Controls.Templates; namespace Avalonia.Controls { @@ -29,7 +30,11 @@ namespace Avalonia.Controls public object? this[object key] { - get => _inner?[key]; + get + { + TryGetValue(key, out var value); + return value; + } set { Inner[key] = value; @@ -119,6 +124,12 @@ namespace Avalonia.Controls Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); } + public void AddDeferred(object key, Func factory) + { + Inner.Add(key, new DeferredItem(factory)); + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + public void Clear() { if (_inner?.Count > 0) @@ -143,10 +154,8 @@ namespace Avalonia.Controls public bool TryGetResource(object key, out object? value) { - if (_inner is not null && _inner.TryGetValue(key, out value)) - { + if (TryGetValue(key, out value)) return true; - } if (_mergedDictionaries != null) { @@ -165,12 +174,28 @@ namespace Avalonia.Controls public bool TryGetValue(object key, out object? value) { - if (_inner is not null) - return _inner.TryGetValue(key, out value); + if (_inner is not null && _inner.TryGetValue(key, out value)) + { + if (value is DeferredItem deffered) + { + _inner[key] = value = deffered.Factory(null) switch + { + ITemplateResult t => t.Result, + object v => v, + _ => null, + }; + } + return true; + } + value = null; return false; } + public IEnumerator> GetEnumerator() + { + return _inner?.GetEnumerator() ?? Enumerable.Empty>().GetEnumerator(); + } void ICollection>.Add(KeyValuePair item) { @@ -198,12 +223,17 @@ namespace Avalonia.Controls return false; } - public IEnumerator> GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + internal bool ContainsDeferredKey(object key) { - return _inner?.GetEnumerator() ?? Enumerable.Empty>().GetEnumerator(); - } + if (_inner is not null && _inner.TryGetValue(key, out var result)) + { + return result is DeferredItem; + } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + return false; + } void IResourceProvider.AddOwner(IResourceHost owner) { @@ -258,5 +288,11 @@ namespace Avalonia.Controls } } } + + private class DeferredItem + { + public DeferredItem(Func factory) => Factory = factory; + public Func Factory { get; } + } } } diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 1758c45650..6121646107 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -132,6 +132,11 @@ namespace Avalonia.Controls { _target.OwnerChanged += OwnerChanged; _owner = _target.Owner; + + if (_owner is object) + { + _owner.ResourcesChanged += ResourcesChanged; + } } protected override void Deinitialize() diff --git a/src/Avalonia.Base/Controls/Templates/ITemplateResult.cs b/src/Avalonia.Base/Controls/Templates/ITemplateResult.cs new file mode 100644 index 0000000000..6bd4d735a7 --- /dev/null +++ b/src/Avalonia.Base/Controls/Templates/ITemplateResult.cs @@ -0,0 +1,8 @@ +namespace Avalonia.Controls.Templates +{ + public interface ITemplateResult + { + public object? Result { get; } + public INameScope NameScope { get; } + } +} diff --git a/src/Avalonia.Controls/Templates/TemplateResult.cs b/src/Avalonia.Base/Controls/Templates/TemplateResult.cs similarity index 80% rename from src/Avalonia.Controls/Templates/TemplateResult.cs rename to src/Avalonia.Base/Controls/Templates/TemplateResult.cs index 770aecc329..0e38c6c0ce 100644 --- a/src/Avalonia.Controls/Templates/TemplateResult.cs +++ b/src/Avalonia.Base/Controls/Templates/TemplateResult.cs @@ -1,9 +1,10 @@ namespace Avalonia.Controls.Templates { - public class TemplateResult + public class TemplateResult : ITemplateResult { public T Result { get; } public INameScope NameScope { get; } + object? ITemplateResult.Result => Result; public TemplateResult(T result, INameScope nameScope) { diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index 1ca70140ec..0d51a6ed36 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -55,13 +55,20 @@ namespace Avalonia.Data.Core.Plugins var methods = type.GetMethods(bindingFlags); - foreach (MethodInfo methodInfo in methods) + foreach (var methodInfo in methods) { if (methodInfo.Name == methodName) { - found = methodInfo; - - break; + var parameters = methodInfo.GetParameters(); + if (parameters.Length == 1 && parameters[0].ParameterType == typeof(object)) + { + found = methodInfo; + break; + } + else if (parameters.Length == 0) + { + found = methodInfo; + } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index 0288dbce91..6232fa39a1 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -42,7 +42,7 @@ namespace Avalonia.Data.Core.Plugins if (target is IObservable result) { return result; - }; + } // If the observable returns a value type then we need to call Observable.Select on it. // First get the type of T in `IObservable`. diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index efcb7dfecb..86e4bffaa8 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -29,21 +29,6 @@ namespace Avalonia { } - /// - /// Initializes a new instance of the class. - /// - /// The property to copy. - /// The new owner type. - /// Optional overridden metadata. - [Obsolete("Use constructor with DirectPropertyBase instead.", true)] - protected DirectPropertyBase( - AvaloniaProperty source, - Type ownerType, - AvaloniaPropertyMetadata metadata) - : this(source as DirectPropertyBase ?? throw new InvalidOperationException(), ownerType, metadata) - { - } - /// /// Initializes a new instance of the class. /// diff --git a/src/Avalonia.Base/EnumExtensions.cs b/src/Avalonia.Base/EnumExtensions.cs index 19eb42a700..9b74266b09 100644 --- a/src/Avalonia.Base/EnumExtensions.cs +++ b/src/Avalonia.Base/EnumExtensions.cs @@ -8,10 +8,6 @@ namespace Avalonia /// public static class EnumExtensions { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Obsolete("This method is obsolete. Use HasAllFlags instead.")] - public static bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum - => value.HasAllFlags(flag); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe bool HasAllFlags(this T value, T flags) where T : unmanaged, Enum diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 889b7e3b82..4f5585ab6c 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -71,7 +71,7 @@ namespace Avalonia.Input.GestureRecognizers { EndGesture(); _tracking = e.Pointer; - _gestureId = ScrollGestureEventArgs.GetNextFreeId();; + _gestureId = ScrollGestureEventArgs.GetNextFreeId(); _trackedRootPoint = e.GetPosition(_target); } } diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index a95d66346a..2ebf01bcf6 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -158,6 +158,8 @@ namespace Avalonia.Input ClearPointerOver(pointer, root, timestamp, position, properties, inputModifiers); } } + + _lastPointer = (pointer, root.PointToScreen(position)); } private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element, @@ -195,7 +197,6 @@ namespace Avalonia.Input } el = root.PointerOverElement = element; - _lastPointer = (pointer, root.PointToScreen(position)); e.RoutedEvent = InputElement.PointerEnteredEvent; diff --git a/src/Avalonia.Base/Input/PointerPoint.cs b/src/Avalonia.Base/Input/PointerPoint.cs index 71145b5cb0..c51f286053 100644 --- a/src/Avalonia.Base/Input/PointerPoint.cs +++ b/src/Avalonia.Base/Input/PointerPoint.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input /// /// Provides basic properties for the input pointer associated with a single mouse, pen/stylus, or touch contact. /// - public sealed class PointerPoint + public struct PointerPoint { public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties) { @@ -33,47 +33,47 @@ namespace Avalonia.Input /// /// Provides extended properties for a PointerPoint object. /// - public sealed class PointerPointProperties + public struct PointerPointProperties { /// /// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device. /// - public bool IsLeftButtonPressed { get; } + public bool IsLeftButtonPressed { get; } = false; /// /// Gets a value that indicates whether the pointer input was triggered by the tertiary action mode of an input device. /// - public bool IsMiddleButtonPressed { get; } + public bool IsMiddleButtonPressed { get; } = false; /// /// Gets a value that indicates whether the pointer input was triggered by the secondary action mode (if supported) of an input device. /// - public bool IsRightButtonPressed { get; } + public bool IsRightButtonPressed { get; } = false; /// /// Gets a value that indicates whether the pointer input was triggered by the first extended mouse button (XButton1). /// - public bool IsXButton1Pressed { get; } + public bool IsXButton1Pressed { get; } = false; /// /// Gets a value that indicates whether the pointer input was triggered by the second extended mouse button (XButton2). /// - public bool IsXButton2Pressed { get; } + public bool IsXButton2Pressed { get; } = false; /// /// Gets a value that indicates whether the barrel button of the pen/stylus device is pressed. /// - public bool IsBarrelButtonPressed { get; } + public bool IsBarrelButtonPressed { get; } = false; /// /// Gets a value that indicates whether the input is from a pen eraser. /// - public bool IsEraser { get; } + public bool IsEraser { get; } = false; /// /// Gets a value that indicates whether the digitizer pen is inverted. /// - public bool IsInverted { get; } + public bool IsInverted { get; } = false; /// /// Gets the clockwise rotation in degrees of a pen device around its own major axis (such as when the user spins the pen in their fingers). @@ -81,7 +81,7 @@ namespace Avalonia.Input /// /// A value between 0.0 and 359.0 in degrees of rotation. The default value is 0.0. /// - public float Twist { get; } + public float Twist { get; } = 0.0F; /// /// Gets a value that indicates the force that the pointer device (typically a pen/stylus) exerts on the surface of the digitizer. @@ -97,7 +97,7 @@ namespace Avalonia.Input /// /// The value is 0.0 when the finger or pen is perpendicular to the digitizer surface, between 0.0 and 90.0 when tilted to the right of perpendicular, and between 0.0 and -90.0 when tilted to the left of perpendicular. The default value is 0.0. /// - public float XTilt { get; } + public float XTilt { get; } = 0.0F; /// /// Gets the plane angle between the X-Z plane and the plane that contains the X axis and the axis of the input device (typically a pen/stylus). @@ -105,14 +105,14 @@ namespace Avalonia.Input /// /// The value is 0.0 when the finger or pen is perpendicular to the digitizer surface, between 0.0 and 90.0 when tilted towards the user, and between 0.0 and -90.0 when tilted away from the user. The default value is 0.0. /// - public float YTilt { get; } + public float YTilt { get; } = 0.0F; /// /// Gets the kind of pointer state change. /// - public PointerUpdateKind PointerUpdateKind { get; } + public PointerUpdateKind PointerUpdateKind { get; } = PointerUpdateKind.LeftButtonPressed; - private PointerPointProperties() + public PointerPointProperties() { } diff --git a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs index 0e4e0ed3e2..b8f6f99ae8 100644 --- a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs @@ -137,9 +137,13 @@ namespace Avalonia.Input.Raw /// public Point Position { get; set; } + /// public float Twist { get; set; } + /// public float Pressure { get; set; } + /// public float XTilt { get; set; } + /// public float YTilt { get; set; } diff --git a/src/Avalonia.Base/Interactivity/RoutedEvent.cs b/src/Avalonia.Base/Interactivity/RoutedEvent.cs index a9b7dc8c89..37edb24cc1 100644 --- a/src/Avalonia.Base/Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Base/Interactivity/RoutedEvent.cs @@ -113,24 +113,6 @@ namespace Avalonia.Interactivity { } - [Obsolete("Use overload taking Action.")] - public IDisposable AddClassHandler( - Func> handler, - RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, - bool handledEventsToo = false) - where TTarget : class, IInteractive - { - void Adapter(object? sender, RoutedEventArgs e) - { - if (sender is TTarget target && e is TEventArgs args) - { - handler(target)(args); - } - } - - return AddClassHandler(typeof(TTarget), Adapter, routes, handledEventsToo); - } - public IDisposable AddClassHandler( Action handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, diff --git a/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs index 3e516b4f97..e6a95d1ffb 100644 --- a/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs @@ -523,7 +523,7 @@ namespace Avalonia.Layout { firstRealizedElement = _elementManager.GetAt(0); firstBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); - firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);; + firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0); int last = _elementManager.GetRealizedElementCount() - 1; lastRealizedElement = _elementManager.GetAt(last); diff --git a/src/Avalonia.Base/Layout/ILayoutManager.cs b/src/Avalonia.Base/Layout/ILayoutManager.cs index 143ce13a1b..88d95c81fd 100644 --- a/src/Avalonia.Base/Layout/ILayoutManager.cs +++ b/src/Avalonia.Base/Layout/ILayoutManager.cs @@ -44,17 +44,6 @@ namespace Avalonia.Layout /// void ExecuteInitialLayoutPass(); - /// - /// Executes the initial layout pass on a layout root. - /// - /// The control to lay out. - /// - /// You should not usually need to call this method explictly, the layout root will call - /// it to carry out the initial layout of the control. - /// - [Obsolete("Call ExecuteInitialLayoutPass without parameter")] - void ExecuteInitialLayoutPass(ILayoutRoot root); - /// /// Registers a control as wanting to receive effective viewport notifications. /// diff --git a/src/Avalonia.Base/Layout/LayoutHelper.cs b/src/Avalonia.Base/Layout/LayoutHelper.cs index 404d19906a..0851dbaea9 100644 --- a/src/Avalonia.Base/Layout/LayoutHelper.cs +++ b/src/Avalonia.Base/Layout/LayoutHelper.cs @@ -251,6 +251,17 @@ namespace Avalonia.Layout { double newValue; + // Round the value to avoid FP errors. This is needed because if `value` has a floating + // point precision error (e.g. 79.333333333333343) then when it's multiplied by + // `dpiScale` and rounded up, it will be rounded up to a value one greater than it + // should be. +#if NET6_0_OR_GREATER + value = Math.Round(value, 8, MidpointRounding.ToZero); +#else + // MidpointRounding.ToZero isn't available in netstandard2.0. + value = Math.Truncate(value * 1e8) / 1e8; +#endif + // If DPI == 1, don't use DPI-aware rounding. if (!MathUtilities.IsOne(dpiScale)) { diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index b9ca6bfbd7..826947c39a 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -196,17 +196,6 @@ namespace Avalonia.Layout ExecuteLayoutPass(); } - [Obsolete("Call ExecuteInitialLayoutPass without parameter")] - public void ExecuteInitialLayoutPass(ILayoutRoot root) - { - if (root != _owner) - { - throw new ArgumentException("ExecuteInitialLayoutPass called with incorrect root."); - } - - ExecuteInitialLayoutPass(); - } - public void Dispose() { _disposed = true; diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 101e867d56..527b63292d 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -460,20 +460,6 @@ namespace Avalonia.Layout _effectiveViewportChanged?.Invoke(this, e); } - /// - /// Marks a property as affecting the control's measurement. - /// - /// The properties. - /// - /// After a call to this method in a control's static constructor, any change to the - /// property will cause to be called on the element. - /// - [Obsolete("Use AffectsMeasure and specify the control type.")] - protected static void AffectsMeasure(params AvaloniaProperty[] properties) - { - AffectsMeasure(properties); - } - /// /// Marks a property as affecting the control's measurement. /// @@ -497,20 +483,6 @@ namespace Avalonia.Layout } } - /// - /// Marks a property as affecting the control's arrangement. - /// - /// The properties. - /// - /// After a call to this method in a control's static constructor, any change to the - /// property will cause to be called on the element. - /// - [Obsolete("Use AffectsArrange and specify the control type.")] - protected static void AffectsArrange(params AvaloniaProperty[] properties) - { - AffectsArrange(properties); - } - /// /// Marks a property as affecting the control's arrangement. /// diff --git a/src/Avalonia.Base/Layout/StackLayout.cs b/src/Avalonia.Base/Layout/StackLayout.cs index 00ac4a37f0..e3c2ab3817 100644 --- a/src/Avalonia.Base/Layout/StackLayout.cs +++ b/src/Avalonia.Base/Layout/StackLayout.cs @@ -322,6 +322,7 @@ namespace Avalonia.Layout protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { + base.OnPropertyChanged(change); if (change.Property == OrientationProperty) { var orientation = change.GetNewValue(); diff --git a/src/Avalonia.Base/Layout/UniformGridLayout.cs b/src/Avalonia.Base/Layout/UniformGridLayout.cs index 47c994a350..a7880a1545 100644 --- a/src/Avalonia.Base/Layout/UniformGridLayout.cs +++ b/src/Avalonia.Base/Layout/UniformGridLayout.cs @@ -473,6 +473,7 @@ namespace Avalonia.Layout protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { + base.OnPropertyChanged(change); if (change.Property == OrientationProperty) { var orientation = change.GetNewValue(); diff --git a/src/Avalonia.Base/Media/DashStyle.cs b/src/Avalonia.Base/Media/DashStyle.cs index abee580020..3a30b2d32f 100644 --- a/src/Avalonia.Base/Media/DashStyle.cs +++ b/src/Avalonia.Base/Media/DashStyle.cs @@ -35,7 +35,6 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// public DashStyle() - : this(null, 0) { } diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 603bb1c1c1..e71f568207 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -228,6 +228,8 @@ namespace Avalonia.Media throw new NotImplementedException(); } + public object? GetFeature(Type t) => null; + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { throw new NotImplementedException(); diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 2a7f3360ad..cae7a8fe75 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -265,7 +265,7 @@ namespace Avalonia.Media //RightToLeft var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null) + if (GlyphClusters != null && GlyphClusters.Count > 0) { if (characterIndex > GlyphClusters[0]) { diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index 5f1617d778..cf8a31c3e9 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -89,22 +89,6 @@ namespace Avalonia.Media.Imaging PlatformImpl.Dispose(); } - /// - /// Initializes a new instance of the class. - /// - /// The pixel format. - /// The pointer to the source bytes. - /// The size of the bitmap in device pixels. - /// The DPI of the bitmap. - /// The number of bytes per row. - [Obsolete("Use overload taking an AlphaFormat.")] - public Bitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) - { - var ri = GetFactory(); - PlatformImpl = RefCountable.Create(ri - .LoadBitmap(format, ri.DefaultAlphaFormat, data, size, dpi, stride)); - } - /// /// Initializes a new instance of the class. /// diff --git a/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs b/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs index 1f39b1344d..1aac8efac7 100644 --- a/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs @@ -9,18 +9,6 @@ namespace Avalonia.Media.Imaging /// public class WriteableBitmap : Bitmap { - /// - /// Initializes a new instance of the class. - /// - /// The size of the bitmap in device pixels. - /// The DPI of the bitmap. - /// The pixel format (optional). - /// An . - [Obsolete("Use overload taking an AlphaFormat.")] - public WriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null) - : base(CreatePlatformImpl(size, dpi, format, null)) - { - } /// /// Initializes a new instance of the class. @@ -30,7 +18,7 @@ namespace Avalonia.Media.Imaging /// The pixel format (optional). /// The alpha format (optional). /// An . - public WriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat) + public WriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null, AlphaFormat? alphaFormat = null) : base(CreatePlatformImpl(size, dpi, format, alphaFormat)) { } diff --git a/src/Avalonia.Base/Media/KnownColors.cs b/src/Avalonia.Base/Media/KnownColors.cs index 8f07b5f8ac..ae2fca5a60 100644 --- a/src/Avalonia.Base/Media/KnownColors.cs +++ b/src/Avalonia.Base/Media/KnownColors.cs @@ -1,10 +1,11 @@ using System; using System.Reflection; using System.Collections.Generic; +using Avalonia.SourceGenerator; namespace Avalonia.Media { - internal static class KnownColors + internal static partial class KnownColors { private static readonly IReadOnlyDictionary _knownColorNames; private static readonly IReadOnlyDictionary _knownColors; @@ -12,23 +13,25 @@ namespace Avalonia.Media private static readonly Dictionary _knownBrushes; #endif + [GenerateEnumValueDictionary()] + private static partial Dictionary GetKnownColors(); + static KnownColors() { var knownColorNames = new Dictionary(StringComparer.OrdinalIgnoreCase); var knownColors = new Dictionary(); - foreach (var field in typeof(KnownColor).GetRuntimeFields()) + foreach (var field in GetKnownColors()) { - if (field.FieldType != typeof(KnownColor)) continue; - var knownColor = (KnownColor)field.GetValue(null)!; + var knownColor = field.Value; if (knownColor == KnownColor.None) continue; - knownColorNames.Add(field.Name, knownColor); + knownColorNames.Add(field.Key, knownColor); // some known colors have the same value, so use the first if (!knownColors.ContainsKey((uint)knownColor)) { - knownColors.Add((uint)knownColor, field.Name); + knownColors.Add((uint)knownColor, field.Key); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index 97df87d3d9..7ab67ea34d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -116,7 +117,30 @@ namespace Avalonia.Media.TextFormatting length = text.Length; } + length = CoerceLength(text, length); + return new ValueSpan(firstTextSourceIndex, length, currentProperties); } + + private static int CoerceLength(ReadOnlySlice text, int length) + { + var finalLength = 0; + + var graphemeEnumerator = new GraphemeEnumerator(text); + + while (graphemeEnumerator.MoveNext()) + { + var grapheme = graphemeEnumerator.Current; + + finalLength += grapheme.Text.Length; + + if (finalLength >= length) + { + return finalLength; + } + } + + return length; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index df83ada34a..a49e4ef13b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -15,6 +15,13 @@ namespace Avalonia.Media.TextFormatting public override void Justify(TextLine textLine) { + var lineImpl = textLine as TextLineImpl; + + if(lineImpl is null) + { + return; + } + var paragraphWidth = Width; if (double.IsInfinity(paragraphWidth)) @@ -22,12 +29,12 @@ namespace Avalonia.Media.TextFormatting return; } - if (textLine.NewLineLength > 0) + if (lineImpl.NewLineLength > 0) { return; } - var textLineBreak = textLine.TextLineBreak; + var textLineBreak = lineImpl.TextLineBreak; if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null) { @@ -39,7 +46,7 @@ namespace Avalonia.Media.TextFormatting var breakOportunities = new Queue(); - foreach (var textRun in textLine.TextRuns) + foreach (var textRun in lineImpl.TextRuns) { var text = textRun.Text; @@ -68,10 +75,10 @@ namespace Avalonia.Media.TextFormatting return; } - var remainingSpace = Math.Max(0, paragraphWidth - textLine.WidthIncludingTrailingWhitespace); + var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace); var spacing = remainingSpace / breakOportunities.Count; - foreach (var textRun in textLine.TextRuns) + foreach (var textRun in lineImpl.TextRuns) { var text = textRun.Text; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index ab72601c3e..42a9e61c36 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -38,7 +38,7 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, + internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var shapeableCharacters = new List(2); @@ -65,7 +65,7 @@ namespace Avalonia.Media.TextFormatting /// The bidi level of the run. /// /// A list of shapeable text runs. - private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, + private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; @@ -76,7 +76,7 @@ namespace Avalonia.Media.TextFormatting { if (script == Script.Common && previousTypeface is not null) { - if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _)) + if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _)) { return new ShapeableTextCharacters(text.Take(fallbackCount), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); @@ -86,10 +86,10 @@ namespace Avalonia.Media.TextFormatting return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface), biDiLevel); } - + if (previousTypeface is not null) { - if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _)) + if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _)) { return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); @@ -106,12 +106,12 @@ namespace Avalonia.Media.TextFormatting { continue; } - + codepoint = codepointEnumerator.Current; - + break; } - + //ToDo: Fix FontFamily fallback var matchFound = FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, @@ -157,14 +157,14 @@ namespace Avalonia.Media.TextFormatting /// /// protected static bool TryGetShapeableLength( - ReadOnlySlice text, - Typeface typeface, + ReadOnlySlice text, + Typeface typeface, Typeface? defaultTypeface, out int length, out Script script) { length = 0; - script = Script.Unknown; + script = Script.Unknown; if (text.Length == 0) { @@ -182,7 +182,7 @@ namespace Avalonia.Media.TextFormatting var currentScript = currentGrapheme.FirstCodepoint.Script; - if (currentScript != Script.Common && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } @@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting { break; } - + if (currentScript != script) { if (script is Script.Unknown || currentScript != Script.Common && diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index f3e8b5969c..0828b6518a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -537,8 +537,13 @@ namespace Avalonia.Media.TextFormatting /// /// The collapsing width. /// The . - private TextCollapsingProperties GetCollapsingProperties(double width) + private TextCollapsingProperties? GetCollapsingProperties(double width) { + if(_textTrimming == TextTrimming.None) + { + return null; + } + return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs index c8a23097db..61b24dc8c5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs @@ -153,7 +153,7 @@ namespace Avalonia.Media.TextFormatting /// /// A value that represents a collapsed line that can be displayed. /// - public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList); + public abstract TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList); /// /// Create a justified line based on justification text properties. diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index f3c62f4994..fa1ab6fd29 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -119,7 +119,7 @@ namespace Avalonia.Media.TextFormatting } /// - public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList) + public override TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList) { if (collapsingPropertiesList.Length == 0) { @@ -128,6 +128,11 @@ namespace Avalonia.Media.TextFormatting var collapsingProperties = collapsingPropertiesList[0]; + if(collapsingProperties is null) + { + return this; + } + var collapsedRuns = collapsingProperties.Collapse(this); if (collapsedRuns is null) @@ -171,7 +176,7 @@ namespace Avalonia.Media.TextFormatting return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); } - if (distance > WidthIncludingTrailingWhitespace) + if (distance >= WidthIncludingTrailingWhitespace) { var lastRun = _textRuns[_textRuns.Count - 1]; @@ -183,8 +188,52 @@ namespace Avalonia.Media.TextFormatting var currentPosition = FirstTextSourceIndex; var currentDistance = 0.0; - foreach (var currentRun in _textRuns) + for (var i = 0; i < _textRuns.Count; i++) { + var currentRun = _textRuns[i]; + + if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) + { + var rightToLeftIndex = i; + currentPosition += currentRun.TextSourceLength; + + while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + { + var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; + + if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight) + { + break; + } + + currentPosition += nextShaped.TextSourceLength; + + rightToLeftIndex++; + } + + for (var j = i; i <= rightToLeftIndex; j++) + { + if(j > _textRuns.Count - 1) + { + break; + } + + currentRun = _textRuns[j]; + + if(currentDistance + currentRun.Size.Width <= distance) + { + currentDistance += currentRun.Size.Width; + currentPosition -= currentRun.TextSourceLength; + + continue; + } + + characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); + + break; + } + } + if (currentDistance + currentRun.Size.Width < distance) { currentDistance += currentRun.Size.Width; @@ -211,12 +260,16 @@ namespace Avalonia.Media.TextFormatting { characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); - var offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + var offset = 0; - if (!shapedRun.GlyphRun.IsLeftToRight) + if (shapedRun.GlyphRun.IsLeftToRight) { - offset = Math.Max(0, offset - shapedRun.Text.End); + offset = Math.Max(0, currentPosition - shapedRun.Text.Start); } + //else + //{ + // offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); + //} characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); @@ -255,10 +308,56 @@ namespace Avalonia.Media.TextFormatting { var currentRun = _textRuns[index]; - if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, - flowDirection, out var distance, out _)) + if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) + { + var i = index; + + var rightToLeftWidth = currentRun.Size.Width; + + while (i + 1 <= _textRuns.Count - 1) + { + var nextRun = _textRuns[i + 1]; + + if (nextRun is ShapedTextCharacters nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight) + { + i++; + + rightToLeftWidth += nextRun.Size.Width; + + continue; + } + + break; + } + + if(i > index) + { + while (i >= index) + { + currentRun = _textRuns[i]; + + rightToLeftWidth -= currentRun.Size.Width; + + if (currentPosition + currentRun.TextSourceLength >= characterIndex) + { + break; + } + + currentPosition += currentRun.TextSourceLength; + + remainingLength -= currentRun.TextSourceLength; + + i--; + } + + currentDistance += rightToLeftWidth; + } + } + + if (currentPosition + currentRun.TextSourceLength >= characterIndex && + TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { - return currentDistance + distance; + return Math.Max(0, currentDistance + distance); } //No hit hit found so we add the full width @@ -283,7 +382,7 @@ namespace Avalonia.Media.TextFormatting distance = currentGlyphRun.Size.Width - distance; } - return currentDistance - distance; + return Math.Max(0, currentDistance - distance); } //No hit hit found so we add the full width @@ -293,7 +392,7 @@ namespace Avalonia.Media.TextFormatting } } - return currentDistance; + return Math.Max(0, currentDistance); } private static bool TryGetDistanceFromCharacterHit( @@ -442,92 +541,139 @@ namespace Avalonia.Media.TextFormatting continue; } - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - - currentPosition += currentRun.TextSourceLength; - - continue; - } - var characterLength = 0; var endX = startX; + var runWidth = 0.0; + TextRunBounds? currentRunBounds = null; - if (currentRun is ShapedTextCharacters currentShapedRun) + var currentShapedRun = currentRun as ShapedTextCharacters; + + if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight) { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + var rightToLeftIndex = index; + startX += currentShapedRun.Size.Width; - currentPosition += offset; + while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + { + var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; - var startIndex = currentRun.Text.Start + offset; + if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) + { + break; + } - double startOffset; - double endOffset; + startX += nextShapedRun.Size.Width; - if (currentShapedRun.ShapedBuffer.IsLeftToRight) + rightToLeftIndex++; + } + + if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds)) { - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + startX = currentRunBounds!.Rectangle.Left; + endX = currentRunBounds.Rectangle.Right; - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + runWidth = currentRunBounds.Rectangle.Width; } - else + + currentDirection = FlowDirection.RightToLeft; + } + else + { + if (currentShapedRun != null) { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + { + startX += currentRun.Size.Width; - if (currentPosition < startIndex) + currentPosition += currentRun.TextSourceLength; + + continue; + } + + var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + + currentPosition += offset; + + var startIndex = currentRun.Text.Start + offset; + + double startOffset; + double endOffset; + + if (currentShapedRun.ShapedBuffer.IsLeftToRight) { - startOffset = endOffset; + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); } else { - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + + if (currentPosition < startIndex) + { + startOffset = endOffset; + } + else + { + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } } - } - startX += startOffset; + startX += startOffset; - endX += endOffset; + endX += endOffset; - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); + characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); - currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ? - FlowDirection.LeftToRight : - FlowDirection.RightToLeft; - } - else - { - if (currentPosition < firstTextSourceIndex) + currentDirection = FlowDirection.LeftToRight; + } + else { - startX += currentRun.Size.Width; + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + { + startX += currentRun.Size.Width; + + currentPosition += currentRun.TextSourceLength; + + continue; + } + + if (currentPosition < firstTextSourceIndex) + { + startX += currentRun.Size.Width; + } + + if (currentPosition + currentRun.TextSourceLength <= characterIndex) + { + endX += currentRun.Size.Width; + + characterLength = currentRun.TextSourceLength; + } } - if (currentPosition + currentRun.TextSourceLength <= characterIndex) + if (endX < startX) { - endX += currentRun.Size.Width; + (endX, startX) = (startX, endX); + } - characterLength = currentRun.TextSourceLength; + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; } - } - if (endX < startX) - { - (endX, startX) = (startX, endX); - } + runWidth = endX - startX; + currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; - } + currentPosition += characterLength; - var runWidth = endX - startX; - var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + remainingLength -= characterLength; + } - if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) + if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0) { if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) { @@ -537,32 +683,26 @@ namespace Avalonia.Media.TextFormatting textBounds.Rectangle = currentRect; - textBounds.TextRunBounds.Add(currentRunBounds); + textBounds.TextRunBounds.Add(currentRunBounds!); } else { - currentRect = currentRunBounds.Rectangle; + currentRect = currentRunBounds!.Rectangle; result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); } } currentWidth += runWidth; - currentPosition += characterLength; + - if (currentPosition > characterIndex) + if (remainingLength <= 0 || currentPosition >= characterIndex) { break; } startX = endX; lastDirection = currentDirection; - remainingLength -= characterLength; - - if (remainingLength <= 0) - { - break; - } } return result; @@ -674,7 +814,7 @@ namespace Avalonia.Media.TextFormatting var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); - if(!MathUtilities.IsZero(runWidth) || NewLineLength > 0) + if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) { if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX)) { @@ -692,7 +832,7 @@ namespace Avalonia.Media.TextFormatting result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); } - } + } currentWidth += runWidth; currentPosition += characterLength; @@ -716,6 +856,107 @@ namespace Avalonia.Media.TextFormatting return result; } + private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds) + { + textRunBounds = null; + + for (var index = runIndex; index >= 0; index--) + { + if (TextRuns[index] is not DrawableTextRun currentRun) + { + continue; + } + + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + { + startX -= currentRun.Size.Width; + + currentPosition += currentRun.TextSourceLength; + + continue; + } + + var characterLength = 0; + var endX = startX; + + if (currentRun is ShapedTextCharacters currentShapedRun) + { + var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + + currentPosition += offset; + + var startIndex = currentRun.Text.Start + offset; + double startOffset; + double endOffset; + + if (currentShapedRun.ShapedBuffer.IsLeftToRight) + { + if (currentPosition < startIndex) + { + startOffset = endOffset = 0; + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + } + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } + + startX -= currentRun.Size.Width - startOffset; + endX -= currentRun.Size.Width - endOffset; + + var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + + characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); + } + else + { + if (currentPosition + currentRun.TextSourceLength <= characterIndex) + { + endX -= currentRun.Size.Width; + } + + if (currentPosition < firstTextSourceIndex) + { + startX -= currentRun.Size.Width; + + characterLength = currentRun.TextSourceLength; + } + } + + if (endX < startX) + { + (endX, startX) = (startX, endX); + } + + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; + } + + var runWidth = endX - startX; + + remainingLength -= characterLength; + + currentPosition += characterLength; + + textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + + return true; + } + + return false; + } + public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) { if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) @@ -1295,6 +1536,11 @@ namespace Avalonia.Media.TextFormatting var textAlignment = _paragraphProperties.TextAlignment; var paragraphFlowDirection = _paragraphProperties.FlowDirection; + if(textAlignment == TextAlignment.Justify) + { + textAlignment = TextAlignment.Start; + } + switch (textAlignment) { case TextAlignment.Start: @@ -1319,12 +1565,12 @@ namespace Avalonia.Media.TextFormatting case TextAlignment.Center: var start = (_paragraphWidth - width) / 2; - if(paragraphFlowDirection == FlowDirection.RightToLeft) + if (paragraphFlowDirection == FlowDirection.RightToLeft) { start -= (widthIncludingTrailingWhitespace - width); } - return Math.Max(0, start); + return Math.Max(0, start); case TextAlignment.Right: return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 56a90f31ea..ab17263806 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -224,7 +224,7 @@ namespace Avalonia.Media.TextFormatting.Unicode } /// - /// Returns if is between + /// Returns if is between /// and , inclusive. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Avalonia.Base/Media/TextTrimming.cs b/src/Avalonia.Base/Media/TextTrimming.cs index b6f5be496f..e2737210be 100644 --- a/src/Avalonia.Base/Media/TextTrimming.cs +++ b/src/Avalonia.Base/Media/TextTrimming.cs @@ -8,7 +8,7 @@ namespace Avalonia.Media /// public abstract class TextTrimming { - public static char s_defaultEllipsisChar = '\u2026'; + internal const char DefaultEllipsisChar = '\u2026'; /// /// Text is not trimmed. @@ -18,22 +18,22 @@ namespace Avalonia.Media /// /// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text. /// - public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, false); + public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(DefaultEllipsisChar, false); /// /// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text. /// - public static TextTrimming WordEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, true); + public static TextTrimming WordEllipsis { get; } = new TextTrailingTrimming(DefaultEllipsisChar, true); /// /// Text is trimmed after a given prefix length. An ellipsis (...) is drawn in between prefix and suffix and represents remaining text. /// - public static TextTrimming PrefixCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 8); + public static TextTrimming PrefixCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(DefaultEllipsisChar, 8); /// /// Text is trimmed at a character boundary starting from the beginning. An ellipsis (...) is drawn in place of remaining text. /// - public static TextTrimming LeadingCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 0); + public static TextTrimming LeadingCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(DefaultEllipsisChar, 0); /// /// Creates properties that will be used for collapsing lines of text. diff --git a/src/Avalonia.Base/Media/UnicodeRange.cs b/src/Avalonia.Base/Media/UnicodeRange.cs index 0077dd64ac..e2338b9b26 100644 --- a/src/Avalonia.Base/Media/UnicodeRange.cs +++ b/src/Avalonia.Base/Media/UnicodeRange.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media /// public readonly struct UnicodeRange { - public static UnicodeRange Default = Parse("0-10FFFD"); + public readonly static UnicodeRange Default = Parse("0-10FFFD"); private readonly UnicodeRangeSegment _single; private readonly IReadOnlyList? _segments = null; diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index d84a509234..6aa5eeea3d 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -172,6 +172,20 @@ namespace Avalonia.Platform /// /// Custom draw operation void Custom(ICustomDrawOperation custom); + + /// + /// Attempts to get an optional feature from the drawing context implementation + /// + object? GetFeature(Type t); + } + + public static class DrawingContextImplExtensions + { + /// + /// Attempts to get an optional feature from the drawing context implementation + /// + public static T? GetFeature(this IDrawingContextImpl context) where T : class => + (T?)context.GetFeature(typeof(T)); } public interface IDrawingContextLayerImpl : IRenderTargetBitmapImpl diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index 5af02219ce..cec20678cf 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -12,6 +12,11 @@ public class BclStorageFile : IStorageBookmarkFile { private readonly FileInfo _fileInfo; + public BclStorageFile(string fileName) + { + _fileInfo = new FileInfo(fileName); + } + public BclStorageFile(FileInfo fileInfo) { _fileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); @@ -27,15 +32,14 @@ public class BclStorageFile : IStorageBookmarkFile public Task GetBasicPropertiesAsync() { - var props = new StorageItemProperties(); if (_fileInfo.Exists) { - props = new StorageItemProperties( + return Task.FromResult(new StorageItemProperties( (ulong)_fileInfo.Length, _fileInfo.CreationTimeUtc, - _fileInfo.LastAccessTimeUtc); + _fileInfo.LastAccessTimeUtc)); } - return Task.FromResult(props); + return Task.FromResult(new StorageItemProperties()); } public Task GetParentAsync() @@ -47,22 +51,22 @@ public class BclStorageFile : IStorageBookmarkFile return Task.FromResult(null); } - public Task OpenRead() + public Task OpenReadAsync() { return Task.FromResult(_fileInfo.OpenRead()); } - public Task OpenWrite() + public Task OpenWriteAsync() { return Task.FromResult(_fileInfo.OpenWrite()); } - public virtual Task SaveBookmark() + public virtual Task SaveBookmarkAsync() { return Task.FromResult(_fileInfo.FullName); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // No-op return Task.CompletedTask; diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 7267017eaf..b91e910777 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Security; using System.Threading.Tasks; using Avalonia.Metadata; @@ -12,6 +14,15 @@ public class BclStorageFolder : IStorageBookmarkFolder { private readonly DirectoryInfo _directoryInfo; + public BclStorageFolder(string path) + { + _directoryInfo = new DirectoryInfo(path); + if (!_directoryInfo.Exists) + { + throw new ArgumentException("Directory must exist"); + } + } + public BclStorageFolder(DirectoryInfo directoryInfo) { _directoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); @@ -43,12 +54,22 @@ public class BclStorageFolder : IStorageBookmarkFolder return Task.FromResult(null); } - public virtual Task SaveBookmark() + public Task> GetItemsAsync() + { + var items = _directoryInfo.GetDirectories() + .Select(d => (IStorageItem)new BclStorageFolder(d)) + .Concat(_directoryInfo.GetFiles().Select(f => new BclStorageFile(f))) + .ToArray(); + + return Task.FromResult>(items); + } + + public virtual Task SaveBookmarkAsync() { return Task.FromResult(_directoryInfo.FullName); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // No-op return Task.CompletedTask; diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs index d21c950862..40f2720ee8 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs @@ -6,7 +6,7 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageBookmarkItem : IStorageItem { - Task ReleaseBookmark(); + Task ReleaseBookmarkAsync(); } [NotClientImplementable] diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 965caf8216..46aa6efa72 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -18,7 +18,7 @@ public interface IStorageFile : IStorageItem /// /// Opens a stream for read access. /// - Task OpenRead(); + Task OpenReadAsync(); /// /// Returns true, if file is writeable. @@ -28,5 +28,5 @@ public interface IStorageFile : IStorageItem /// /// Opens stream for writing to the file. /// - Task OpenWrite(); + Task OpenWriteAsync(); } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 25b9f01a92..0ffb9f41c6 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -1,4 +1,6 @@ -using Avalonia.Metadata; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Metadata; namespace Avalonia.Platform.Storage; @@ -8,4 +10,11 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageFolder : IStorageItem { + /// + /// Gets the files and subfolders in the current folder. + /// + /// + /// When this method completes successfully, it returns a list of the files and folders in the current folder. Each item in the list is represented by an implementation object. + /// + Task> GetItemsAsync(); } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs index 8513ebc7d9..f5469d31c9 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs @@ -44,7 +44,7 @@ public interface IStorageItem : IDisposable /// /// Returns identifier of a bookmark. Can be null if OS denied request. /// - Task SaveBookmark(); + Task SaveBookmarkAsync(); /// /// Gets the parent folder of the current storage item. diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index 19d316eb85..a6db4330a3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -1,12 +1,9 @@ // ReSharper disable InconsistentNaming // ReSharper disable CheckNamespace -using System; -using System.Collections.Generic; using System.Numerics; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.Composition.Transport; // Special license applies License.md @@ -16,10 +13,10 @@ namespace Avalonia.Rendering.Composition.Animations /// This is the base class for ExpressionAnimation and KeyFrameAnimation. /// /// - /// Use the method to start the animation. + /// Use the method to start the animation. /// Value parameters (as opposed to reference parameters which are set using ) /// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called. - /// Changing the value of the variable after is called will not affect + /// Changing the value of the variable after is called will not affect /// the value of the ExpressionAnimation. /// See the remarks section of ExpressionAnimation for additional information. /// diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs index 577910d975..ec2972044e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -16,7 +16,7 @@ namespace Avalonia.Rendering.Composition.Animations /// This contrasts s, which use an interpolator to define how the animating /// property changes over time. The mathematical equation can be defined using references to properties /// of Composition objects, mathematical functions and operators and Input. - /// Use the method to start the animation. + /// Use the method to start the animation. /// public class ExpressionAnimation : CompositionAnimation { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs index 4692fde5e3..d21a4d06e3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs @@ -24,9 +24,9 @@ namespace Avalonia.Rendering.Composition.Animations /// The delay behavior of the key frame animation. /// public AnimationDelayBehavior DelayBehavior { get; set; } - + /// - /// Delay before the animation starts after is called. + /// Delay before the animation starts after is called. /// public System.TimeSpan DelayTime { get; set; } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index db773bc43c..e47c8e7e31 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -29,6 +29,7 @@ public class CompositingRenderer : IRendererWithCompositor private bool _queuedUpdate; private Action _update; private Action _invalidateScene; + private bool _updating; internal CompositionTarget CompositionTarget; @@ -71,12 +72,14 @@ public class CompositingRenderer : IRendererWithCompositor if(_queuedUpdate) return; _queuedUpdate = true; - Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition); + _compositor.InvokeWhenReadyForNextCommit(_update); } /// public void AddDirty(IVisual visual) { + if (_updating) + throw new InvalidOperationException("Visual was invalidated during the render pass"); _dirty.Add((Visual)visual); QueueUpdate(); } @@ -84,7 +87,16 @@ public class CompositingRenderer : IRendererWithCompositor /// public IEnumerable HitTest(Point p, IVisual root, Func? filter) { - var res = CompositionTarget.TryHitTest(p, filter); + Func? f = null; + if (filter != null) + f = v => + { + if (v is CompositionDrawListVisual dlv) + return filter(dlv.Visual); + return true; + }; + + var res = CompositionTarget.TryHitTest(p, f); if(res == null) yield break; foreach(var v in res) @@ -107,6 +119,8 @@ public class CompositingRenderer : IRendererWithCompositor /// public void RecalculateChildren(IVisual visual) { + if (_updating) + throw new InvalidOperationException("Visual was invalidated during the render pass"); _recalculateChildren.Add((Visual)visual); QueueUpdate(); } @@ -191,7 +205,7 @@ public class CompositingRenderer : IRendererWithCompositor private void InvalidateScene() => SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); - private void Update() + private void UpdateCore() { _queuedUpdate = false; foreach (var visual in _dirty) @@ -240,6 +254,21 @@ public class CompositingRenderer : IRendererWithCompositor CompositionTarget.Scaling = _root.RenderScaling; Compositor.InvokeOnNextCommit(_invalidateScene); } + + private void Update() + { + if(_updating) + return; + _updating = true; + try + { + UpdateCore(); + } + finally + { + _updating = false; + } + } public void Resized(Size size) { diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index 77b392eee5..b019d1792b 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -54,13 +54,11 @@ internal class CompositionDrawListVisual : CompositionContainerVisual Visual = visual; } - internal override bool HitTest(Point pt, Func? filter) + internal override bool HitTest(Point pt) { var custom = Visual as ICustomHitTest; if (DrawList == null && custom == null) return false; - if (filter != null && !filter(Visual)) - return false; if (custom != null) { // Simulate the old behavior diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 4e53e163ec..eb499604e0 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -31,7 +31,7 @@ namespace Avalonia.Rendering.Composition /// /// /// - public PooledList? TryHitTest(Point point, Func? filter) + public PooledList? TryHitTest(Point point, Func? filter) { Server.Readback.NextRead(); if (Root == null) @@ -88,10 +88,14 @@ namespace Avalonia.Rendering.Composition } void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList result, - Func? filter) + Func? filter) { if (visual.Visible == false) return; + + if (filter != null && !filter(visual)) + return; + if (!TryTransformTo(visual, globalPoint, out var point)) return; @@ -111,7 +115,7 @@ namespace Avalonia.Rendering.Composition } // Hit-test the current node - if (visual.HitTest(point, filter)) + if (visual.HitTest(point)) result.Add(visual); } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 45212d0f36..10360f7874 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -33,6 +33,7 @@ namespace Avalonia.Rendering.Composition internal IEasing DefaultEasing { get; } private List? _invokeOnNextCommit; private readonly Stack> _invokeListPool = new(); + private Task? _lastBatchCompleted; /// /// Creates a new compositor on a specified render loop that would use a particular GPU @@ -86,7 +87,7 @@ namespace Avalonia.Rendering.Composition if (_invokeOnNextCommit != null) ScheduleCommitCallbacks(batch.Completed); - return batch.Completed; + return _lastBatchCompleted = batch.Completed; } async void ScheduleCommitCallbacks(Task task) @@ -139,5 +140,15 @@ namespace Avalonia.Rendering.Composition _invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new(); _invokeOnNextCommit.Add(action); } + + public void InvokeWhenReadyForNextCommit(Action action) + { + if (_lastBatchCompleted == null || _lastBatchCompleted.IsCompleted) + Dispatcher.UIThread.Post(action, DispatcherPriority.Composition); + else + _lastBatchCompleted.ContinueWith( + static (_, state) => Dispatcher.UIThread.Post((Action)state!, DispatcherPriority.Composition), + action); + } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index 01216d19ed..30b57883fc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -156,6 +156,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW ++_drawOperationIndex; } + public object? GetFeature(Type t) => null; + /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 7eb35a68ed..03859d241f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -155,6 +156,8 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.Custom(custom); } + public object? GetFeature(Type t) => _impl.GetFeature(t); + public class VisualBrushRenderer : IVisualBrushRenderer { public CompositionDrawList? VisualBrushDrawList { get; set; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index c20594aaca..5c1ac0312c 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -78,6 +78,13 @@ namespace Avalonia.Rendering.Composition.Server if (Root == null) return; + + if ((_renderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + { + _renderTarget!.Dispose(); + _renderTarget = null; + } + _renderTarget ??= _renderTargetFactory(); Compositor.UpdateServerTime(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 621bc84f4a..bfc2b2d626 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -108,6 +108,7 @@ namespace Avalonia.Rendering.Composition.Server private void RenderCore() { ApplyPendingBatches(); + CompletePendingBatches(); foreach(var animation in _activeAnimations) _animationsToUpdate.Add(animation); @@ -119,8 +120,6 @@ namespace Avalonia.Rendering.Composition.Server foreach (var t in _activeTargets) t.Render(); - - CompletePendingBatches(); } public void AddCompositionTarget(ServerCompositionTarget target) diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 7356b7b9e8..6d6818256a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -53,6 +53,6 @@ namespace Avalonia.Rendering.Composition internal object? Tag { get; set; } - internal virtual bool HitTest(Point point, Func? filter) => true; + internal virtual bool HitTest(Point point) => true; } } diff --git a/src/Avalonia.Base/Rendering/DirtyVisuals.cs b/src/Avalonia.Base/Rendering/DirtyVisuals.cs index 00bc236b9c..999b12e810 100644 --- a/src/Avalonia.Base/Rendering/DirtyVisuals.cs +++ b/src/Avalonia.Base/Rendering/DirtyVisuals.cs @@ -17,8 +17,7 @@ namespace Avalonia.Rendering { private SortedDictionary> _inner = new SortedDictionary>(); private Dictionary _index = new Dictionary(); - private List _deferredChanges = new List(); - private int _deferring; + private int _enumerating; /// /// Gets the number of dirty visuals. @@ -31,10 +30,9 @@ namespace Avalonia.Rendering /// The dirty visual. public void Add(IVisual visual) { - if (_deferring > 0) + if (_enumerating > 0) { - _deferredChanges.Add(visual); - return; + throw new InvalidOperationException("Visual was invalidated during a render pass"); } var distance = visual.CalculateDistanceFromAncestor(visual.VisualRoot); @@ -65,7 +63,7 @@ namespace Avalonia.Rendering /// public void Clear() { - if (_deferring > 0) + if (_enumerating > 0) { throw new InvalidOperationException("Cannot clear while enumerating"); } @@ -80,7 +78,7 @@ namespace Avalonia.Rendering /// A collection of visuals. public IEnumerator GetEnumerator() { - BeginDefer(); + _enumerating++; try { foreach (var i in _inner) @@ -93,27 +91,10 @@ namespace Avalonia.Rendering } finally { - EndDefer(); + _enumerating--; } } - - private void BeginDefer() - { - ++_deferring; - } - - private void EndDefer() - { - if (--_deferring > 0) return; - - foreach (var visual in _deferredChanges) - { - Add(visual); - } - - _deferredChanges.Clear(); - } - + /// /// Gets the dirty visuals, in ascending order of distance to their root. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 07082e4ac3..d6766fa9b8 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -203,6 +203,8 @@ namespace Avalonia.Rendering.SceneGraph ++_drawOperationindex; } + public object? GetFeature(Type t) => null; + /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 4b43f93aee..cf53b86fa7 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { @@ -19,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The geometry. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public GeometryNode(Matrix transform, IBrush? brush, IPen? pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 1f58111ecf..1d85e95835 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; - using Avalonia.Media; -using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { @@ -19,7 +15,7 @@ namespace Avalonia.Rendering.SceneGraph /// The transform. /// The foreground brush. /// The glyph run to draw. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, IBrush foreground, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index ee5ec0a5fc..0af8ba2752 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { @@ -19,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The start point of the line. /// The end point of the line. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public LineNode( Matrix transform, IPen pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 549c1fd7de..5fd200ddff 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -17,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// The opacity mask to push. /// The bounds of the mask. - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) : base(Rect.Empty, Matrix.Identity, aux) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index a9d1bf96e5..f2ffd7411c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The rectangle to draw. /// The box shadow parameters - /// Child scenes for drawing visual brushes. + /// Auxiliary data required to draw the brush. public RectangleNode( Matrix transform, IBrush? brush, diff --git a/src/Avalonia.Base/Rotate3DTransform.cs b/src/Avalonia.Base/Rotate3DTransform.cs index 2c4e515861..2de33889c8 100644 --- a/src/Avalonia.Base/Rotate3DTransform.cs +++ b/src/Avalonia.Base/Rotate3DTransform.cs @@ -205,6 +205,7 @@ public class Rotate3DTransform : Transform protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { + base.OnPropertyChanged(change); if (!_isInitializing) RaiseChanged(); } } diff --git a/src/Avalonia.Base/Utilities/IWeakSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakSubscriber.cs index 613c931165..2a5b8d39c5 100644 --- a/src/Avalonia.Base/Utilities/IWeakSubscriber.cs +++ b/src/Avalonia.Base/Utilities/IWeakSubscriber.cs @@ -3,7 +3,7 @@ using System; namespace Avalonia.Utilities { /// - /// Defines a listener to a event subscribed vis the . + /// Defines a listener to a event subscribed vis the . /// /// The type of the event arguments. public interface IWeakSubscriber where T : EventArgs diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 3d5be806e1..3c48c3469e 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -255,7 +255,7 @@ namespace Avalonia.Utilities /// /// Clamps a value between a minimum and maximum value. /// - /// The value. + /// The value. /// The minimum value. /// The maximum value. /// The clamped value. @@ -324,6 +324,41 @@ namespace Avalonia.Utilities return angle * 2 * Math.PI; } + /// + /// Calculates the point of an angle on an ellipse. + /// + /// The centre point of the ellipse. + /// The x radius of the ellipse. + /// The y radius of the ellipse. + /// The angle in radians. + /// A point on the ellipse. + public static Point GetEllipsePoint(Point centre, double radiusX, double radiusY, double angle) + { + return new Point(radiusX * Math.Cos(angle) + centre.X, radiusY * Math.Sin(angle) + centre.Y); + } + + /// + /// Gets the minimum and maximum from the specified numbers. + /// + /// The first number. + /// The second number. + /// A tuple containing the minimum and maximum of the two specified numbers. + public static (double min, double max) GetMinMax(double a, double b) + { + return a < b ? (a, b) : (b, a); + } + + /// + /// Gets the minimum and maximum from the specified number and the difference with that number. + /// + /// The initial value to use. + /// The difference for . + /// A tuple containing the minimum and maximum of the specified number and the difference with that number. + public static (double min, double max) GetMinMaxFromDelta(double initialValue, double delta) + { + return GetMinMax(initialValue, initialValue + delta); + } + private static void ThrowCannotBeGreaterThanException(T min, T max) { throw new ArgumentException($"{min} cannot be greater than {max}."); diff --git a/src/Avalonia.Base/Utilities/WeakObservable.cs b/src/Avalonia.Base/Utilities/WeakObservable.cs index 6bf1d4082f..e1c350d539 100644 --- a/src/Avalonia.Base/Utilities/WeakObservable.cs +++ b/src/Avalonia.Base/Utilities/WeakObservable.cs @@ -9,31 +9,6 @@ namespace Avalonia.Utilities /// public static class WeakObservable { - /// - /// Converts a .NET event conforming to the standard .NET event pattern into an observable - /// sequence, subscribing weakly. - /// - /// The type of target. - /// The type of the event args. - /// Object instance that exposes the event to convert. - /// Name of the event to convert. - /// - [Obsolete("Use WeakEvent-based overload")] - public static IObservable> FromEventPattern( - TTarget target, - string eventName) - where TEventArgs : EventArgs - { - _ = target ?? throw new ArgumentNullException(nameof(target)); - _ = eventName ?? throw new ArgumentNullException(nameof(eventName)); - - return Observable.Create>(observer => - { - var handler = new Handler(observer); - WeakSubscriptionManager.Subscribe(target, eventName, handler); - return () => WeakSubscriptionManager.Unsubscribe(target, eventName, handler); - }).Publish().RefCount(); - } private class Handler : IWeakSubscriber, diff --git a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs deleted file mode 100644 index dc9e86cc32..0000000000 --- a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; - -namespace Avalonia.Utilities -{ - /// - /// Manages subscriptions to events using weak listeners. - /// - public static class WeakSubscriptionManager - { - /// - /// Subscribes to an event on an object using a weak subscription. - /// - /// The type of the target. - /// The type of the event arguments. - /// The event source. - /// The name of the event. - /// The subscriber. - [Obsolete("Use WeakEvent")] - public static void Subscribe(TTarget target, string eventName, IWeakSubscriber subscriber) - where TEventArgs : EventArgs - { - _ = target ?? throw new ArgumentNullException(nameof(target)); - - var dic = SubscriptionTypeStorage.Subscribers.GetOrCreateValue(target); - - if (!dic.TryGetValue(eventName, out var sub)) - { - dic[eventName] = sub = new Subscription(dic, typeof(TTarget), target, eventName); - } - - sub.Add(new WeakReference>(subscriber)); - } - - /// - /// Unsubscribes from an event. - /// - /// The type of the event arguments. - /// The event source. - /// The name of the event. - /// The subscriber. - public static void Unsubscribe(object target, string eventName, IWeakSubscriber subscriber) - where T : EventArgs - { - if (SubscriptionTypeStorage.Subscribers.TryGetValue(target, out var dic)) - { - if (dic.TryGetValue(eventName, out var sub)) - { - sub.Remove(subscriber); - } - } - } - - private static class SubscriptionTypeStorage - where T : EventArgs - { - public static readonly ConditionalWeakTable> Subscribers - = new ConditionalWeakTable>(); - } - - private class SubscriptionDic : Dictionary> - where T : EventArgs - { - } - - private static readonly Dictionary> Accessors - = new Dictionary>(); - - private class Subscription where T : EventArgs - { - private readonly EventInfo _info; - private readonly SubscriptionDic _sdic; - private readonly object _target; - private readonly string _eventName; - private readonly Delegate _delegate; - - private WeakReference>?[] _data = new WeakReference>?[16]; - private int _count = 0; - - public Subscription(SubscriptionDic sdic, Type targetType, object target, string eventName) - { - _sdic = sdic; - _target = target; - _eventName = eventName; - if (!Accessors.TryGetValue(targetType, out var evDic)) - Accessors[targetType] = evDic = new Dictionary(); - - if (evDic.TryGetValue(eventName, out var info)) - { - _info = info; - } - else - { - var ev = targetType.GetRuntimeEvents().FirstOrDefault(x => x.Name == eventName); - - if (ev == null) - { - throw new ArgumentException( - $"The event {eventName} was not found on {target.GetType()}."); - } - - evDic[eventName] = _info = ev; - } - - var del = new Action(OnEvent); - _delegate = del.GetMethodInfo().CreateDelegate(_info.EventHandlerType!, del.Target); - _info.AddMethod!.Invoke(target, new[] { _delegate }); - } - - void Destroy() - { - _info.RemoveMethod!.Invoke(_target, new[] { _delegate }); - _sdic.Remove(_eventName); - } - - public void Add(WeakReference> s) - { - if (_count == _data.Length) - { - //Extend capacity - var ndata = new WeakReference>?[_data.Length*2]; - Array.Copy(_data, ndata, _data.Length); - _data = ndata; - } - _data[_count] = s!; - _count++; - } - - public void Remove(IWeakSubscriber s) - { - var removed = false; - - for (int c = 0; c < _count; ++c) - { - var reference = _data[c]; - IWeakSubscriber? instance; - - if (reference != null && reference.TryGetTarget(out instance) && instance == s) - { - _data[c] = null; - removed = true; - } - } - - if (removed) - { - Compact(); - } - } - - void Compact() - { - int empty = -1; - for (int c = 0; c < _count; c++) - { - var r = _data[c]; - //Mark current index as first empty - if (r == null && empty == -1) - empty = c; - //If current element isn't null and we have an empty one - if (r != null && empty != -1) - { - _data[c] = null; - _data[empty] = r; - empty++; - } - } - if (empty != -1) - _count = empty; - if (_count == 0) - Destroy(); - } - - void OnEvent(object sender, T eventArgs) - { - var needCompact = false; - for(var c=0; c<_count; c++) - { - var r = _data[c]; - if (r?.TryGetTarget(out var sub) == true) - sub!.OnEvent(sender, eventArgs); - else - needCompact = true; - } - if (needCompact) - Compact(); - } - } - } -} diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 8feba116f0..69389def56 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -338,22 +338,6 @@ namespace Avalonia Contract.Requires(context != null); } - /// - /// Indicates that a property change should cause to be - /// called. - /// - /// The properties. - /// - /// This method should be called in a control's static constructor with each property - /// on the control which when changed should cause a redraw. This is similar to WPF's - /// FrameworkPropertyMetadata.AffectsRender flag. - /// - [Obsolete("Use AffectsRender and specify the control type.")] - protected static void AffectsRender(params AvaloniaProperty[] properties) - { - AffectsRender(properties); - } - /// /// Indicates that a property change should cause to be /// called. diff --git a/src/Avalonia.Base/VisualTree/IVisualTreeHost.cs b/src/Avalonia.Base/VisualTree/IVisualTreeHost.cs deleted file mode 100644 index e47377063f..0000000000 --- a/src/Avalonia.Base/VisualTree/IVisualTreeHost.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Avalonia.VisualTree -{ - /// - /// Interface for controls that host their own separate visual tree, such as popups. - /// - [Obsolete] - public interface IVisualTreeHost - { - /// - /// Gets the root of the hosted visual tree. - /// - /// - /// The root of the hosted visual tree. - /// - IVisual? Root { get; } - } -} diff --git a/src/Avalonia.Base/VisualTree/VisualExtensions.cs b/src/Avalonia.Base/VisualTree/VisualExtensions.cs index 128a39c082..a1b8894604 100644 --- a/src/Avalonia.Base/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualTree/VisualExtensions.cs @@ -413,7 +413,7 @@ namespace Avalonia.VisualTree Index = index, ZIndex = element.ZIndex, }) - .OrderBy(x => x, null) + .OrderBy(x => x, ZOrderElement.Comparer) .Select(x => x.Element!); } @@ -448,6 +448,19 @@ namespace Avalonia.VisualTree public int Index { get; set; } public int ZIndex { get; set; } + class ZOrderComparer : IComparer + { + public int Compare(ZOrderElement? x, ZOrderElement? y) + { + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(null, y)) return 1; + if (ReferenceEquals(null, x)) return -1; + return x.CompareTo(y); + } + } + + public static IComparer Comparer { get; } = new ZOrderComparer(); + public int CompareTo(ZOrderElement? other) { if (other is null) diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index dc9064b262..eef574d297 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -108,4 +108,5 @@ + diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 39a83c00c4..5915388822 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -326,8 +326,8 @@ namespace Avalonia.Build.Tasks var op = i[c].Operand as MethodReference; // TODO: Throw an error - // This usually happens when same XAML resource was added twice for some weird reason - // We currently support it for dual-named default theme resource + // This usually happens when the same XAML resource was added twice for some weird reason + // We currently support it for dual-named default theme resources if (op != null && op.Name == TrampolineName) { diff --git a/src/Avalonia.Controls.ColorPicker/Converters/DoNothingForNullConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/DoNothingForNullConverter.cs new file mode 100644 index 0000000000..421a5acb28 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/DoNothingForNullConverter.cs @@ -0,0 +1,35 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converter that will do nothing (not update bound values) when a null value is encountered. + /// This converter enables binding nullable with non-nullable properties in some scenarios. + /// + public class DoNothingForNullConverter : IValueConverter + { + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return value ?? BindingOperations.DoNothing; + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return value ?? BindingOperations.DoNothing; + } + } +} + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml deleted file mode 100644 index 35cd7a9faa..0000000000 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml deleted file mode 100644 index db1fa3ee4e..0000000000 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 907b00dfff..74a1df4991 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -3,9 +3,6 @@ xmlns:controls="using:Avalonia.Controls" x:CompileBindings="True"> - - 5,5,0,0 - @@ -25,7 +22,7 @@ Padding="0,0,10,0" UseLayoutRounding="False"> - @@ -45,7 +42,7 @@ - + + SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"> + + + 5,5,0,0 + + @@ -81,10 +83,4 @@ - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 993745b1e5..dab4142001 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -11,6 +11,7 @@ + 48 30 @@ -77,6 +78,8 @@ 17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5 13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z + + 3 @@ -97,7 +100,7 @@ HorizontalAlignment="Stretch" VerticalAlignment="Top" Background="{DynamicResource SystemControlBackgroundBaseLowBrush}" - CornerRadius="{TemplateBinding CornerRadius}" /> + CornerRadius="{DynamicResource ColorViewTabBackgroundCornerRadius}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index a5a94a2322..0a1a9e60c6 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -11,15 +11,20 @@ Stretch="Uniform" DestinationRect="0,0,8,8"> - - - - - - + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml similarity index 89% rename from src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml index c3bc7df4a4..e067fe4ab5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml @@ -1,15 +1,14 @@ - + - - - 80 - 40 - + + 80 + 40 - + - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSlider.xaml new file mode 100644 index 0000000000..9aa2dcd9f9 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSlider.xaml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSpectrum.xaml similarity index 69% rename from src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSpectrum.xaml index 0e57f6b483..0e137c89c6 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSpectrum.xaml @@ -1,9 +1,10 @@ - + - - - - + + + - - - + + + - - - - - + + + + + - + - - - + + + - + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml new file mode 100644 index 0000000000..f21b03cee2 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs index ff222658db..ab73ed73c7 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -12,9 +12,6 @@ namespace Avalonia.Collections { public virtual string PropertyPath => null; - [Obsolete("Use Direction property to read or override sorting direction.")] - public virtual bool Descending => Direction == ListSortDirection.Descending; - public virtual ListSortDirection Direction => ListSortDirection.Ascending; public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath); public abstract IComparer Comparer { get; } @@ -254,13 +251,6 @@ namespace Avalonia.Collections return new DataGridPathSortDescription(propertyPath, direction, null, culture); } - - [Obsolete("Use overload taking a ListSortDirection.")] - public static DataGridSortDescription FromPath(string propertyPath, bool descending, CultureInfo culture = null) - { - return new DataGridPathSortDescription(propertyPath, descending ? ListSortDirection.Descending : ListSortDirection.Ascending, null, culture); - } - public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction, IComparer comparer) { return new DataGridPathSortDescription(propertyPath, direction, comparer, null); diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index d42468f47e..cacd5c5a16 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2167,7 +2167,23 @@ namespace Avalonia.Controls return desiredSize; } + + /// + protected override void OnDataContextBeginUpdate() + { + base.OnDataContextBeginUpdate(); + NotifyDataContextPropertyForAllRowCells(GetAllRows(), true); + } + + /// + protected override void OnDataContextEndUpdate() + { + base.OnDataContextEndUpdate(); + + NotifyDataContextPropertyForAllRowCells(GetAllRows(), false); + } + /// /// Raises the BeginningEdit event. /// @@ -3165,6 +3181,20 @@ namespace Avalonia.Controls } } + private static void NotifyDataContextPropertyForAllRowCells(IEnumerable rowSource, bool arg2) + { + foreach (DataGridRow row in rowSource) + { + foreach (DataGridCell cell in row.Cells) + { + if (cell.Content is StyledElement cellContent) + { + DataContextProperty.Notifying?.Invoke(cellContent, arg2); + } + } + } + } + private void UpdateRowDetailsVisibilityMode(DataGridRowDetailsVisibilityMode newDetailsMode) { int itemCount = DataConnection.Count; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index d63c738133..69e6766bfd 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -23,10 +23,10 @@ namespace Avalonia.Controls [PseudoClasses(":pressed", ":current", ":expanded")] public class DataGridRowGroupHeader : TemplatedControl { - private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton"; - private const string DATAGRIDROWGROUPHEADER_indentSpacer = "IndentSpacer"; - private const string DATAGRIDROWGROUPHEADER_itemCountElement = "ItemCountElement"; - private const string DATAGRIDROWGROUPHEADER_propertyNameElement = "PropertyNameElement"; + private const string DATAGRIDROWGROUPHEADER_expanderButton = "PART_ExpanderButton"; + private const string DATAGRIDROWGROUPHEADER_indentSpacer = "PART_IndentSpacer"; + private const string DATAGRIDROWGROUPHEADER_itemCountElement = "PART_ItemCountElement"; + private const string DATAGRIDROWGROUPHEADER_propertyNameElement = "PART_PropertyNameElement"; private bool _areIsCheckedHandlersSuspended; private ToggleButton _expanderButton; diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index 5ae83427b5..b4bae02b9b 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -53,7 +53,6 @@ - - @@ -258,10 +258,10 @@ ColumnDefinitions="Auto,Auto,Auto,Auto" RowDefinitions="Auto,*,Auto"> - - + Theme="{StaticResource SimpleDataGridRowGroupExpanderButtonTheme}" /> - - diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 5c95932c1f..c675139831 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -392,6 +392,8 @@ namespace Avalonia.Controls private AutoCompleteSelector? _itemSelector; private AutoCompleteSelector? _textSelector; + private readonly EventHandler _populateDropDownHandler; + public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); @@ -668,6 +670,7 @@ namespace Avalonia.Controls if (newValue == TimeSpan.Zero) { + _delayTimer.Tick -= _populateDropDownHandler; _delayTimer = null; } } @@ -678,7 +681,7 @@ namespace Avalonia.Controls if (_delayTimer == null) { _delayTimer = new DispatcherTimer(); - _delayTimer.Tick += PopulateDropDown; + _delayTimer.Tick += _populateDropDownHandler; } // Set the new tick interval @@ -864,6 +867,7 @@ namespace Avalonia.Controls /// public AutoCompleteBox() { + _populateDropDownHandler = PopulateDropDown; ClearView(); } @@ -1771,10 +1775,7 @@ namespace Avalonia.Controls /// The event arguments. private void PopulateDropDown(object? sender, EventArgs e) { - if (_delayTimer != null) - { - _delayTimer.Stop(); - } + _delayTimer?.Stop(); // Update the prefix/search text. SearchText = Text; diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 6dba33516b..8e5d4e1e06 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Windows.Input; using Avalonia.Automation.Peers; @@ -281,24 +282,29 @@ namespace Avalonia.Controls /// protected override void OnKeyDown(KeyEventArgs e) { - if (e.Key == Key.Enter) + switch (e.Key) { - OnClick(); - e.Handled = true; - } - else if (e.Key == Key.Space) - { - if (ClickMode == ClickMode.Press) - { + case Key.Enter: OnClick(); + e.Handled = true; + break; + + case Key.Space: + { + if (ClickMode == ClickMode.Press) + { + OnClick(); + } + + IsPressed = true; + e.Handled = true; + break; } - IsPressed = true; - e.Handled = true; - } - else if (e.Key == Key.Escape && Flyout != null) - { - // If Flyout doesn't have focusable content, close the flyout here - Flyout.Hide(); + + case Key.Escape when Flyout != null: + // If Flyout doesn't have focusable content, close the flyout here + CloseFlyout(); + break; } base.OnKeyDown(e); @@ -327,7 +333,14 @@ namespace Avalonia.Controls { if (IsEffectivelyEnabled) { - OpenFlyout(); + if (_isFlyoutOpen) + { + CloseFlyout(); + } + else + { + OpenFlyout(); + } var e = new RoutedEventArgs(ClickEvent); RaiseEvent(e); @@ -348,6 +361,14 @@ namespace Avalonia.Controls Flyout?.ShowAt(this); } + /// + /// Closes the button's flyout. + /// + protected virtual void CloseFlyout() + { + Flyout?.Hide(); + } + /// /// Invoked when the button's flyout is opened. /// @@ -494,8 +515,7 @@ namespace Avalonia.Controls // If flyout is changed while one is already open, make sure we // close the old one first - if (oldFlyout != null && - oldFlyout.IsOpen) + if (oldFlyout != null && oldFlyout.IsOpen) { oldFlyout.Hide(); } diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 2dbb5f02f9..0de068a416 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls /// /// [TemplatePart(PART_ElementMonth, typeof(CalendarItem))] - [TemplatePart(PART_ElementRoot, typeof(Panel))] + [TemplatePart(PART_ElementRoot, typeof(Panel))] public class Calendar : TemplatedControl { internal const int RowsPerMonth = 7; @@ -338,14 +338,11 @@ namespace Avalonia.Controls /// The DependencyPropertyChangedEventArgs. private void OnIsTodayHighlightedChanged(AvaloniaPropertyChangedEventArgs e) { - if (DisplayDate != null) - { - int i = DateTimeHelper.CompareYearMonth(DisplayDateInternal, DateTime.Today); + int i = DateTimeHelper.CompareYearMonth(DisplayDateInternal, DateTime.Today); - if (i > -2 && i < 2) - { - UpdateMonths(); - } + if (i > -2 && i < 2) + { + UpdateMonths(); } } @@ -655,7 +652,7 @@ namespace Avalonia.Controls SelectedDatesChanged?.Invoke(this, e); } } - + internal Collection RemovedItems { get; set; } internal DateTime? LastSelectedDateInternal { get; set; } internal DateTime? LastSelectedDate @@ -914,7 +911,7 @@ namespace Avalonia.Controls o => o.DisplayDateEnd, (o, v) => o.DisplayDateEnd = v, defaultBindingMode: BindingMode.TwoWay); - + /// /// Gets or sets the last date to be displayed. /// @@ -1242,7 +1239,7 @@ namespace Avalonia.Controls { b.IsSelected = false; } - } + } } } } @@ -1278,7 +1275,7 @@ namespace Avalonia.Controls internal void OnPreviousClick() { - if (DisplayMode == CalendarMode.Month && DisplayDate != null) + if (DisplayMode == CalendarMode.Month) { DateTime? d = DateTimeHelper.AddMonths(DateTimeHelper.DiscardDayTime(DisplayDate), -1); if (d.HasValue) @@ -1326,7 +1323,7 @@ namespace Avalonia.Controls } internal void OnNextClick() { - if (DisplayMode == CalendarMode.Month && DisplayDate != null) + if (DisplayMode == CalendarMode.Month) { DateTime? d = DateTimeHelper.AddMonths(DateTimeHelper.DiscardDayTime(DisplayDate), 1); if (d.HasValue) @@ -1645,7 +1642,7 @@ namespace Avalonia.Controls { if (DisplayMode == CalendarMode.Month) { - if (LastSelectedDate.HasValue && DisplayDateInternal != null) + if (LastSelectedDate.HasValue) { // If a blackout day is inactive, when clicked on it, the // previous inactive day which is not a blackout day can get @@ -1897,24 +1894,21 @@ namespace Avalonia.Controls { case CalendarMode.Month: { - if (DisplayDate != null) - { - DateTime? selectedDate = new DateTime(DisplayDateInternal.Year, DisplayDateInternal.Month, 1); + DateTime? selectedDate = new DateTime(DisplayDateInternal.Year, DisplayDateInternal.Month, 1); - if (DateTimeHelper.CompareYearMonth(DateTime.MaxValue, selectedDate.Value) > 0) - { - // since DisplayDate is not equal to - // DateTime.MaxValue we are sure selectedDate is\ - // not null - selectedDate = DateTimeHelper.AddMonths(selectedDate.Value, 1)!.Value; - selectedDate = DateTimeHelper.AddDays(selectedDate.Value, -1)!.Value; - } - else - { - selectedDate = DateTime.MaxValue; - } - ProcessSelection(shift, selectedDate, null); + if (DateTimeHelper.CompareYearMonth(DateTime.MaxValue, selectedDate.Value) > 0) + { + // since DisplayDate is not equal to + // DateTime.MaxValue we are sure selectedDate is\ + // not null + selectedDate = DateTimeHelper.AddMonths(selectedDate.Value, 1)!.Value; + selectedDate = DateTimeHelper.AddDays(selectedDate.Value, -1)!.Value; } + else + { + selectedDate = DateTime.MaxValue; + } + ProcessSelection(shift, selectedDate, null); break; } case CalendarMode.Year: @@ -2026,7 +2020,6 @@ namespace Avalonia.Controls focusDate = DisplayDate; LastSelectedDate = DisplayDate; } - Debug.Assert(focusDate != null, "focusDate should not be null!"); FocusButton = FindDayButtonFromDay(focusDate); if (FocusButton != null) @@ -2091,17 +2084,17 @@ namespace Avalonia.Controls static Calendar() { - IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnIsEnabledChanged(e)); - FirstDayOfWeekProperty.Changed.AddClassHandler((x,e) => x.OnFirstDayOfWeekChanged(e)); - IsTodayHighlightedProperty.Changed.AddClassHandler((x,e) => x.OnIsTodayHighlightedChanged(e)); - DisplayModeProperty.Changed.AddClassHandler((x,e) => x.OnDisplayModePropertyChanged(e)); - SelectionModeProperty.Changed.AddClassHandler((x,e) => x.OnSelectionModeChanged(e)); - SelectedDateProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateChanged(e)); - DisplayDateProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateChanged(e)); - DisplayDateStartProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateStartChanged(e)); - DisplayDateEndProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateEndChanged(e)); - KeyDownEvent.AddClassHandler((x,e) => x.Calendar_KeyDown(e)); - KeyUpEvent.AddClassHandler((x,e) => x.Calendar_KeyUp(e)); + IsEnabledProperty.Changed.AddClassHandler((x, e) => x.OnIsEnabledChanged(e)); + FirstDayOfWeekProperty.Changed.AddClassHandler((x, e) => x.OnFirstDayOfWeekChanged(e)); + IsTodayHighlightedProperty.Changed.AddClassHandler((x, e) => x.OnIsTodayHighlightedChanged(e)); + DisplayModeProperty.Changed.AddClassHandler((x, e) => x.OnDisplayModePropertyChanged(e)); + SelectionModeProperty.Changed.AddClassHandler((x, e) => x.OnSelectionModeChanged(e)); + SelectedDateProperty.Changed.AddClassHandler((x, e) => x.OnSelectedDateChanged(e)); + DisplayDateProperty.Changed.AddClassHandler((x, e) => x.OnDisplayDateChanged(e)); + DisplayDateStartProperty.Changed.AddClassHandler((x, e) => x.OnDisplayDateStartChanged(e)); + DisplayDateEndProperty.Changed.AddClassHandler((x, e) => x.OnDisplayDateEndChanged(e)); + KeyDownEvent.AddClassHandler((x, e) => x.Calendar_KeyDown(e)); + KeyUpEvent.AddClassHandler((x, e) => x.Calendar_KeyUp(e)); } /// @@ -2116,8 +2109,8 @@ namespace Avalonia.Controls RemovedItems = new Collection(); } - private const string PART_ElementRoot = "Root"; - private const string PART_ElementMonth = "CalendarItem"; + private const string PART_ElementRoot = "PART_Root"; + private const string PART_ElementMonth = "PART_CalendarItem"; /// /// Builds the visual tree for the diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 4c958c83b7..e2eabb5f28 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -4,7 +4,6 @@ // All other rights reserved. using System; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using Avalonia.Collections.Pooled; @@ -33,11 +32,11 @@ namespace Avalonia.Controls.Primitives /// private const int NumberOfDaysPerWeek = 7; - private const string PART_ElementHeaderButton = "HeaderButton"; - private const string PART_ElementPreviousButton = "PreviousButton"; - private const string PART_ElementNextButton = "NextButton"; - private const string PART_ElementMonthView = "MonthView"; - private const string PART_ElementYearView = "YearView"; + private const string PART_ElementHeaderButton = "PART_HeaderButton"; + private const string PART_ElementPreviousButton = "PART_PreviousButton"; + private const string PART_ElementNextButton = "PART_NextButton"; + private const string PART_ElementMonthView = "PART_MonthView"; + private const string PART_ElementYearView = "PART_YearView"; private Button? _headerButton; private Button? _nextButton; @@ -353,7 +352,6 @@ namespace Avalonia.Controls.Primitives { if (Owner != null) { - Debug.Assert(Owner.DisplayDate != null, "The Owner Calendar's DisplayDate should not be null!"); _currentMonth = Owner.DisplayDateInternal; } else @@ -361,17 +359,14 @@ namespace Avalonia.Controls.Primitives _currentMonth = DateTime.Today; } - if (_currentMonth != null) - { - SetMonthModeHeaderButton(); - SetMonthModePreviousButton(_currentMonth); - SetMonthModeNextButton(_currentMonth); + SetMonthModeHeaderButton(); + SetMonthModePreviousButton(_currentMonth); + SetMonthModeNextButton(_currentMonth); - if (MonthView != null) - { - SetDayTitles(); - SetCalendarDayButtons(_currentMonth); - } + if (MonthView != null) + { + SetDayTitles(); + SetCalendarDayButtons(_currentMonth); } } private void SetMonthModeHeaderButton() @@ -592,7 +587,6 @@ namespace Avalonia.Controls.Primitives { if (Owner != null) { - Debug.Assert(Owner.SelectedMonth != null, "The Owner Calendar's SelectedMonth should not be null!"); _currentMonth = (DateTime)Owner.SelectedMonth; } else @@ -600,16 +594,13 @@ namespace Avalonia.Controls.Primitives _currentMonth = DateTime.Today; } - if (_currentMonth != null) - { - SetYearModeHeaderButton(); - SetYearModePreviousButton(); - SetYearModeNextButton(); + SetYearModeHeaderButton(); + SetYearModePreviousButton(); + SetYearModeNextButton(); - if (YearView != null) - { - SetMonthButtonsForYearMode(); - } + if (YearView != null) + { + SetMonthButtonsForYearMode(); } } private void SetYearModeHeaderButton() @@ -660,7 +651,6 @@ namespace Avalonia.Controls.Primitives childButton.IsCalendarButtonFocused = false; } - Debug.Assert(Owner.DisplayDateInternal != null, "The Owner Calendar's DisplayDateInternal should not be null!"); childButton.IsSelected = (DateTimeHelper.CompareYearMonth(day, Owner.DisplayDateInternal) == 0); if (DateTimeHelper.CompareYearMonth(day, Owner.DisplayDateRangeStart) < 0 || DateTimeHelper.CompareYearMonth(day, Owner.DisplayDateRangeEnd) > 0) @@ -685,7 +675,6 @@ namespace Avalonia.Controls.Primitives if (Owner != null) { - Debug.Assert(Owner.SelectedYear != null, "The owning Calendar's selected year should not be null!"); selectedYear = Owner.SelectedYear; _currentMonth = (DateTime)Owner.SelectedMonth; } @@ -695,19 +684,16 @@ namespace Avalonia.Controls.Primitives selectedYear = DateTime.Today; } - if (_currentMonth != null) - { - int decade = DateTimeHelper.DecadeOfDate(selectedYear); - int decadeEnd = DateTimeHelper.EndOfDecade(selectedYear); + int decade = DateTimeHelper.DecadeOfDate(selectedYear); + int decadeEnd = DateTimeHelper.EndOfDecade(selectedYear); - SetDecadeModeHeaderButton(decade, decadeEnd); - SetDecadeModePreviousButton(decade); - SetDecadeModeNextButton(decadeEnd); + SetDecadeModeHeaderButton(decade, decadeEnd); + SetDecadeModePreviousButton(decade); + SetDecadeModeNextButton(decadeEnd); - if (YearView != null) - { - SetYearButtons(decade, decadeEnd); - } + if (YearView != null) + { + SetYearButtons(decade, decadeEnd); } } internal void UpdateYearViewSelection(CalendarButton calendarButton) @@ -822,22 +808,15 @@ namespace Avalonia.Controls.Primitives { if (Owner.DisplayMode == CalendarMode.Month) { - if (Owner.DisplayDate != null) - { - d = Owner.DisplayDateInternal; - Owner.SelectedMonth = new DateTime(d.Year, d.Month, 1); - } + d = Owner.DisplayDateInternal; + Owner.SelectedMonth = new DateTime(d.Year, d.Month, 1); Owner.DisplayMode = CalendarMode.Year; } else { Debug.Assert(Owner.DisplayMode == CalendarMode.Year, "The Owner Calendar's DisplayMode should be Year!"); - - if (Owner.SelectedMonth != null) - { - d = Owner.SelectedMonth; - Owner.SelectedYear = new DateTime(d.Year, d.Month, 1); - } + d = Owner.SelectedMonth; + Owner.SelectedYear = new DateTime(d.Year, d.Month, 1); Owner.DisplayMode = CalendarMode.Decade; } } diff --git a/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs b/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs index c7c35718e0..f4bc2528ba 100644 --- a/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs +++ b/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs @@ -297,7 +297,7 @@ namespace Avalonia.Controls.Primitives } else { - if (item != null && DateTime.Compare(this[index], item) != 0 && Calendar.IsValidDateSelection(_owner, item)) + if (DateTime.Compare(this[index], item) != 0 && Calendar.IsValidDateSelection(_owner, item)) { removedItems.Add(this[index]); base.SetItem(index, item); diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 083182a370..524362fcf9 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Rendering; using Avalonia.Styling; @@ -104,7 +105,6 @@ namespace Avalonia.Controls private static readonly HashSet _loadedQueue = new HashSet(); private static readonly HashSet _loadedProcessingQueue = new HashSet(); - private bool _isAttachedToVisualTree = false; private bool _isLoaded = false; private DataTemplates? _dataTemplates; private IControl? _focusAdorner; @@ -347,7 +347,7 @@ namespace Avalonia.Controls internal void OnLoadedCore() { if (_isLoaded == false && - _isAttachedToVisualTree) + ((ILogical)this).IsAttachedToLogicalTree) { _isLoaded = true; OnLoaded(); @@ -395,7 +395,6 @@ namespace Avalonia.Controls protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTreeCore(e); - _isAttachedToVisualTree = true; InitializeIfNeeded(); @@ -406,7 +405,6 @@ namespace Avalonia.Controls protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTreeCore(e); - _isAttachedToVisualTree = false; OnUnloadedCore(); } diff --git a/src/Avalonia.Controls/Converters/StringFormatConverter.cs b/src/Avalonia.Controls/Converters/StringFormatConverter.cs index ae920dac7e..82e75d9c34 100644 --- a/src/Avalonia.Controls/Converters/StringFormatConverter.cs +++ b/src/Avalonia.Controls/Converters/StringFormatConverter.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using Avalonia.Data; using Avalonia.Data.Converters; namespace Avalonia.Controls.Converters; @@ -15,13 +14,17 @@ public class StringFormatConverter : IMultiValueConverter { public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { - try + if (values[0] is string format) { - return string.Format((string)values[0]!, values.Skip(1).ToArray()); - } - catch (Exception e) - { - return new BindingNotification(e, BindingErrorType.Error); + try + { + return string.Format(format, values.Skip(1).ToArray()); + } + catch + { + return AvaloniaProperty.UnsetValue; + } } + return AvaloniaProperty.UnsetValue; } } diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index f2b808fe0d..fb27feb521 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -14,15 +14,15 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a date /// - [TemplatePart("ButtonContentGrid", typeof(Grid))] - [TemplatePart("DayText", typeof(TextBlock))] - [TemplatePart("FirstSpacer", typeof(Rectangle))] - [TemplatePart("FlyoutButton", typeof(Button))] - [TemplatePart("MonthText", typeof(TextBlock))] - [TemplatePart("PickerPresenter", typeof(DatePickerPresenter))] - [TemplatePart("Popup", typeof(Popup))] - [TemplatePart("SecondSpacer", typeof(Rectangle))] - [TemplatePart("YearText", typeof(TextBlock))] + [TemplatePart("PART_ButtonContentGrid", typeof(Grid))] + [TemplatePart("PART_DayTextBlock", typeof(TextBlock))] + [TemplatePart("PART_FirstSpacer", typeof(Rectangle))] + [TemplatePart("PART_FlyoutButton", typeof(Button))] + [TemplatePart("PART_MonthTextBlock", typeof(TextBlock))] + [TemplatePart("PART_PickerPresenter", typeof(DatePickerPresenter))] + [TemplatePart("PART_Popup", typeof(Popup))] + [TemplatePart("PART_SecondSpacer", typeof(Rectangle))] + [TemplatePart("PART_YearTextBlock", typeof(TextBlock))] [PseudoClasses(":hasnodate")] public class DatePicker : TemplatedControl { @@ -280,15 +280,15 @@ namespace Avalonia.Controls } base.OnApplyTemplate(e); - _flyoutButton = e.NameScope.Find - [TemplatePart("AcceptButton", typeof(Button))] - [TemplatePart("DayDownButton", typeof(RepeatButton))] - [TemplatePart("DayHost", typeof(Panel))] - [TemplatePart("DaySelector", typeof(DateTimePickerPanel))] - [TemplatePart("DayUpButton", typeof(RepeatButton))] - [TemplatePart("DismissButton", typeof(Button))] - [TemplatePart("FirstSpacer", typeof(Rectangle))] - [TemplatePart("MonthDownButton", typeof(RepeatButton))] - [TemplatePart("MonthHost", typeof(Panel))] - [TemplatePart("MonthSelector", typeof(DateTimePickerPanel))] - [TemplatePart("MonthUpButton", typeof(RepeatButton))] - [TemplatePart("PickerContainer", typeof(Grid))] - [TemplatePart("SecondSpacer", typeof(Rectangle))] - [TemplatePart("YearDownButton", typeof(RepeatButton))] - [TemplatePart("YearHost", typeof(Panel))] - [TemplatePart("YearSelector", typeof(DateTimePickerPanel))] - [TemplatePart("YearUpButton", typeof(RepeatButton))] + [TemplatePart("PART_AcceptButton", typeof(Button))] + [TemplatePart("PART_DayDownButton", typeof(RepeatButton))] + [TemplatePart("PART_DayHost", typeof(Panel))] + [TemplatePart("PART_DaySelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_DayUpButton", typeof(RepeatButton))] + [TemplatePart("PART_DismissButton", typeof(Button))] + [TemplatePart("PART_FirstSpacer", typeof(Rectangle))] + [TemplatePart("PART_MonthDownButton", typeof(RepeatButton))] + [TemplatePart("PART_MonthHost", typeof(Panel))] + [TemplatePart("PART_MonthSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_MonthUpButton", typeof(RepeatButton))] + [TemplatePart("PART_PickerContainer", typeof(Grid))] + [TemplatePart("PART_SecondSpacer", typeof(Rectangle))] + [TemplatePart("PART_YearDownButton", typeof(RepeatButton))] + [TemplatePart("PART_YearHost", typeof(Panel))] + [TemplatePart("PART_YearSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_YearUpButton", typeof(RepeatButton))] public class DatePickerPresenter : PickerPresenterBase { /// @@ -253,58 +253,58 @@ namespace Avalonia.Controls { base.OnApplyTemplate(e); // These are requirements, so throw if not found - _pickerContainer = e.NameScope.Get("PickerContainer"); - _monthHost = e.NameScope.Get("MonthHost"); - _dayHost = e.NameScope.Get("DayHost"); - _yearHost = e.NameScope.Get("YearHost"); + _pickerContainer = e.NameScope.Get("PART_PickerContainer"); + _monthHost = e.NameScope.Get("PART_MonthHost"); + _dayHost = e.NameScope.Get("PART_DayHost"); + _yearHost = e.NameScope.Get("PART_YearHost"); - _monthSelector = e.NameScope.Get("MonthSelector"); + _monthSelector = e.NameScope.Get("PART_MonthSelector"); _monthSelector.SelectionChanged += OnMonthChanged; - _daySelector = e.NameScope.Get("DaySelector"); + _daySelector = e.NameScope.Get("PART_DaySelector"); _daySelector.SelectionChanged += OnDayChanged; - _yearSelector = e.NameScope.Get("YearSelector"); + _yearSelector = e.NameScope.Get("PART_YearSelector"); _yearSelector.SelectionChanged += OnYearChanged; - _acceptButton = e.NameScope.Get - [TemplatePart("AcceptButton", typeof(Button))] - [TemplatePart("DismissButton", typeof(Button))] - [TemplatePart("HourDownButton", typeof(RepeatButton))] - [TemplatePart("HourSelector", typeof(DateTimePickerPanel))] - [TemplatePart("HourUpButton", typeof(RepeatButton))] - [TemplatePart("MinuteDownButton", typeof(RepeatButton))] - [TemplatePart("MinuteSelector", typeof(DateTimePickerPanel))] - [TemplatePart("MinuteUpButton", typeof(RepeatButton))] - [TemplatePart("PeriodDownButton", typeof(RepeatButton))] - [TemplatePart("PeriodHost", typeof(Panel))] - [TemplatePart("PeriodSelector", typeof(DateTimePickerPanel))] - [TemplatePart("PeriodUpButton", typeof(RepeatButton))] - [TemplatePart("PickerContainer", typeof(Grid))] - [TemplatePart("SecondSpacer", typeof(Rectangle))] + [TemplatePart("PART_AcceptButton", typeof(Button))] + [TemplatePart("PART_DismissButton", typeof(Button))] + [TemplatePart("PART_HourDownButton", typeof(RepeatButton))] + [TemplatePart("PART_HourSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_HourUpButton", typeof(RepeatButton))] + [TemplatePart("PART_MinuteDownButton", typeof(RepeatButton))] + [TemplatePart("PART_MinuteSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_MinuteUpButton", typeof(RepeatButton))] + [TemplatePart("PART_PeriodDownButton", typeof(RepeatButton))] + [TemplatePart("PART_PeriodHost", typeof(Panel))] + [TemplatePart("PART_PeriodSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_PeriodUpButton", typeof(RepeatButton))] + [TemplatePart("PART_PickerContainer", typeof(Grid))] + [TemplatePart("PART_SecondSpacer", typeof(Rectangle))] public class TimePickerPresenter : PickerPresenterBase { /// @@ -122,40 +122,40 @@ namespace Avalonia.Controls { base.OnApplyTemplate(e); - _pickerContainer = e.NameScope.Get("PickerContainer"); - _periodHost = e.NameScope.Get("PeriodHost"); + _pickerContainer = e.NameScope.Get("PART_PickerContainer"); + _periodHost = e.NameScope.Get("PART_PeriodHost"); - _hourSelector = e.NameScope.Get("HourSelector"); - _minuteSelector = e.NameScope.Get("MinuteSelector"); - _periodSelector = e.NameScope.Get("PeriodSelector"); + _hourSelector = e.NameScope.Get("PART_HourSelector"); + _minuteSelector = e.NameScope.Get("PART_MinuteSelector"); + _periodSelector = e.NameScope.Get("PART_PeriodSelector"); - _spacer2 = e.NameScope.Get("SecondSpacer"); + _spacer2 = e.NameScope.Get("PART_SecondSpacer"); - _acceptButton = e.NameScope.Get