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/build/SkiaSharp.props b/build/SkiaSharp.props index cc573825cd..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 af8c53cb33..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); 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/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/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index 1f4d1e6018..045ba4a059 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -1,6 +1,7 @@  @@ -97,6 +98,17 @@ + + + + + + + + + + diff --git a/samples/IntegrationTestApp/MacOSIntegration.cs b/samples/IntegrationTestApp/MacOSIntegration.cs new file mode 100644 index 0000000000..f700a5b4e2 --- /dev/null +++ b/samples/IntegrationTestApp/MacOSIntegration.cs @@ -0,0 +1,27 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Controls; + +namespace IntegrationTestApp +{ + public static class MacOSIntegration + { + [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "sel_registerName")] + private static extern IntPtr GetHandle(string name); + + [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")] + private static extern long Int64_objc_msgSend(IntPtr receiver, IntPtr selector); + + private static readonly IntPtr s_orderedIndexSelector; + + static MacOSIntegration() + { + s_orderedIndexSelector = GetHandle("orderedIndex");; + } + + public static long GetOrderedIndex(Window window) + { + return Int64_objc_msgSend(window.PlatformImpl!.Handle.Handle, s_orderedIndexSelector); + } + } +} diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 2085b5da2b..f72f83fcb8 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.Linq; using Avalonia; +using Avalonia.Automation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; +using Microsoft.CodeAnalysis; namespace IntegrationTestApp { @@ -63,6 +65,17 @@ namespace IntegrationTestApp WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex, }; + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + // Make sure the windows have unique names and AutomationIds. + var existing = lifetime.Windows.OfType().Count(); + if (existing > 0) + { + AutomationProperties.SetAutomationId(window, window.Name + (existing + 1)); + window.Title += $" {existing + 1}"; + } + } + if (size.HasValue) { window.Width = size.Value.Width; diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 4001bac7e2..17c359df51 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -3,7 +3,7 @@ x:Class="IntegrationTestApp.ShowWindowTest" Name="SecondaryWindow" Title="Show Window Test"> - + @@ -31,6 +31,10 @@ Maximized Fullscreen - + + + + + 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/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs index 6bb06367de..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; @@ -179,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)) { @@ -200,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() ), }; /// @@ -232,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; @@ -251,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(); @@ -271,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; @@ -281,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)); @@ -291,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/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/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/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/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/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 9aa3c25425..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; @@ -77,6 +78,8 @@ public class CompositingRenderer : IRendererWithCompositor /// 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/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.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/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 59cc48975f..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 @@ -205,7 +206,7 @@ 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 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml b/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml index 8d54cb93ca..7042f51c71 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml @@ -23,11 +23,11 @@ - - - - + @@ -188,62 +181,62 @@ Padding="{DynamicResource DateTimeFlyoutBorderPadding}" MaxHeight="398" CornerRadius="{TemplateBinding CornerRadius}"> - + - + - - - + + - + - - - + + - + - - - + + - - - + @@ -251,7 +244,7 @@ VerticalAlignment="Top" Fill="{DynamicResource DatePickerFlyoutPresenterSpacerFill}" Grid.ColumnSpan="2"/> - - - - + @@ -216,42 +209,42 @@ Padding="{DynamicResource DateTimeFlyoutBorderPadding}" MaxHeight="398"> - + - + - - - + + - + - - - + + - + - - - + + - - - - - - - + @@ -197,50 +197,50 @@ CornerRadius="{TemplateBinding CornerRadius}"> - + - + - - - - + - - - - + - - - - - - - - - + @@ -214,50 +214,50 @@ CornerRadius="{TemplateBinding CornerRadius}"> - + - - - - - - - - - - - - @@ -272,12 +272,12 @@ Color="{DynamicResource ThemeAccentColor}" /> - - - -