diff --git a/.editorconfig b/.editorconfig index 7995062f9f..ff7ac5d69e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -140,6 +140,8 @@ dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomme # CS1591: Missing XML comment for publicly visible type or member dotnet_diagnostic.CS1591.severity = suggestion +# CS0162: Remove unreachable code +dotnet_diagnostic.CS0162.severity = error # CA1304: Specify CultureInfo dotnet_diagnostic.CA1304.severity = warning # CA1802: Use literals where appropriate diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml b/samples/ControlCatalog/Pages/CompositionPage.axaml index 403f45b8eb..602b9b768d 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml @@ -2,44 +2,57 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:pages="using:ControlCatalog.Pages" x:Class="ControlCatalog.Pages.CompositionPage"> - - Implicit animations + + + - - - - - - - - - - - - - - - - - - - - - - Resize me - - + + + + + + + + + + + + + + + + + + + + + Resize me + + - - - + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs index c70675b606..8b12a2d663 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -1,28 +1,39 @@ using System; using System.Collections.Generic; +using System.Numerics; +using System.Threading; using Avalonia; +using Avalonia.Animation; using Avalonia.Controls; +using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Animations; using Avalonia.VisualTree; +using Math = System.Math; namespace ControlCatalog.Pages; public partial class CompositionPage : UserControl { private ImplicitAnimationCollection? _implicitAnimations; + private CompositionCustomVisual? _customVisual; + private CompositionSolidColorVisual? _solidVisual; public CompositionPage() { AvaloniaXamlLoader.Load(this); + AttachAnimatedSolidVisual(this.FindControl("SolidVisualHost")!); + AttachCustomVisual(this.FindControl("CustomVisualHost")!); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); this.Get("Items").Items = CreateColorItems(); + } private static List CreateColorItems() @@ -126,6 +137,167 @@ public partial class CompositionPage : UserControl compositionVisual.ImplicitAnimations = page._implicitAnimations; } } + + void AttachAnimatedSolidVisual(Visual v) + { + void Update() + { + if(_solidVisual == null) + return; + _solidVisual.Size = new Vector2((float)v.Bounds.Width / 3, (float)v.Bounds.Height / 3); + _solidVisual.Offset = new Vector3((float)v.Bounds.Width / 3, (float)v.Bounds.Height / 3, 0); + } + v.AttachedToVisualTree += delegate + { + var compositor = ElementComposition.GetElementVisual(v)?.Compositor; + if(compositor == null || _solidVisual?.Compositor == compositor) + return; + _solidVisual = compositor.CreateSolidColorVisual(); + ElementComposition.SetElementChildVisual(v, _solidVisual); + _solidVisual.Color = Colors.Red; + var animation = _solidVisual.Compositor.CreateColorKeyFrameAnimation(); + animation.InsertKeyFrame(0, Colors.Red); + animation.InsertKeyFrame(0.5f, Colors.Blue); + animation.InsertKeyFrame(1, Colors.Green); + animation.Duration = TimeSpan.FromSeconds(5); + animation.IterationBehavior = AnimationIterationBehavior.Forever; + animation.Direction = PlaybackDirection.Alternate; + _solidVisual.StartAnimation("Color", animation); + + _solidVisual.AnchorPoint = new Vector2(0, 0); + + var scale = _solidVisual.Compositor.CreateVector3KeyFrameAnimation(); + scale.Duration = TimeSpan.FromSeconds(5); + scale.IterationBehavior = AnimationIterationBehavior.Forever; + scale.InsertKeyFrame(0, new Vector3(1, 1, 0)); + scale.InsertKeyFrame(0.5f, new Vector3(1.5f, 1.5f, 0)); + scale.InsertKeyFrame(1, new Vector3(1, 1, 0)); + + _solidVisual.StartAnimation("Scale", scale); + + var center = + _solidVisual.Compositor.CreateExpressionAnimation( + "Vector3(this.Target.Size.X * 0.5, this.Target.Size.Y * 0.5, 1)"); + _solidVisual.StartAnimation("CenterPoint", center); + Update(); + }; + v.PropertyChanged += (_, a) => + { + if (a.Property == BoundsProperty) + Update(); + }; + } + + void AttachCustomVisual(Visual v) + { + void Update() + { + if (_customVisual == null) + return; + var h = (float)Math.Min(v.Bounds.Height, v.Bounds.Width / 3); + _customVisual.Size = new Vector2((float)v.Bounds.Width, h); + _customVisual.Offset = new Vector3(0, (float)(v.Bounds.Height - h) / 2, 0); + } + v.AttachedToVisualTree += delegate + { + var compositor = ElementComposition.GetElementVisual(v)?.Compositor; + if(compositor == null || _customVisual?.Compositor == compositor) + return; + _customVisual = compositor.CreateCustomVisual(new CustomVisualHandler()); + ElementComposition.SetElementChildVisual(v, _customVisual); + _customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage); + Update(); + }; + + v.PropertyChanged += (_, a) => + { + if (a.Property == BoundsProperty) + Update(); + }; + } + + class CustomVisualHandler : CompositionCustomVisualHandler + { + private TimeSpan _animationElapsed; + private TimeSpan? _lastServerTime; + private bool _running; + + public static readonly object StopMessage = new(), StartMessage = new(); + + public override void OnRender(ImmediateDrawingContext drawingContext) + { + if (_running) + { + if (_lastServerTime.HasValue) _animationElapsed += (CompositionNow - _lastServerTime.Value); + _lastServerTime = CompositionNow; + } + + const int cnt = 20; + var maxPointSizeX = EffectiveSize.X / (cnt * 1.6); + var maxPointSizeY = EffectiveSize.Y / 4; + var pointSize = Math.Min(maxPointSizeX, maxPointSizeY); + var animationLength = TimeSpan.FromSeconds(4); + var animationStage = _animationElapsed.TotalSeconds / animationLength.TotalSeconds; + + var sinOffset = Math.Cos(_animationElapsed.TotalSeconds) * 1.5; + + for (var c = 0; c < cnt; c++) + { + var stage = (animationStage + (double)c / cnt) % 1; + var colorStage = + (animationStage + (Math.Sin(_animationElapsed.TotalSeconds * 2) + 1) / 2 + (double)c / cnt) % 1; + var posX = (EffectiveSize.X + pointSize * 3) * stage - pointSize; + var posY = (EffectiveSize.Y - pointSize) * (1 + Math.Sin(stage * 3.14 * 3 + sinOffset)) / 2 + pointSize / 2; + var opacity = Math.Sin(stage * 3.14); + + + drawingContext.DrawEllipse(new ImmutableSolidColorBrush(Color.FromArgb( + 255, + (byte)(255 - 255 * colorStage), + (byte)(255 * Math.Abs(0.5 - colorStage) * 2), + (byte)(255 * colorStage) + ), opacity), null, + new Point(posX, posY), pointSize / 2, pointSize / 2); + } + + } + + public override void OnMessage(object message) + { + if (message == StartMessage) + { + _running = true; + _lastServerTime = null; + RegisterForNextAnimationFrameUpdate(); + } + else if (message == StopMessage) + _running = false; + } + + public override void OnAnimationFrameUpdate() + { + if (_running) + { + Invalidate(); + RegisterForNextAnimationFrameUpdate(); + } + } + } + + private void ButtonThreadSleep(object? sender, RoutedEventArgs e) + { + Thread.Sleep(10000); + } + + private void ButtonStartCustomVisual(object? sender, RoutedEventArgs e) + { + _customVisual?.SendHandlerMessage(CustomVisualHandler.StartMessage); + } + + private void ButtonStopCustomVisual(object? sender, RoutedEventArgs e) + { + _customVisual?.SendHandlerMessage(CustomVisualHandler.StopMessage); + } } public class CompositionPageColorItem diff --git a/src/Avalonia.Base/Input/KeyEventArgs.cs b/src/Avalonia.Base/Input/KeyEventArgs.cs index 39c9766105..35fa549995 100644 --- a/src/Avalonia.Base/Input/KeyEventArgs.cs +++ b/src/Avalonia.Base/Input/KeyEventArgs.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input { public class KeyEventArgs : RoutedEventArgs { - internal KeyEventArgs() + public KeyEventArgs() { } diff --git a/src/Avalonia.Base/Input/TextInputEventArgs.cs b/src/Avalonia.Base/Input/TextInputEventArgs.cs index 787bf1abd3..a027bec0c6 100644 --- a/src/Avalonia.Base/Input/TextInputEventArgs.cs +++ b/src/Avalonia.Base/Input/TextInputEventArgs.cs @@ -4,7 +4,7 @@ namespace Avalonia.Input { public class TextInputEventArgs : RoutedEventArgs { - internal TextInputEventArgs() + public TextInputEventArgs() { } diff --git a/src/Avalonia.Base/Media/Brush.cs b/src/Avalonia.Base/Media/Brush.cs index 8d531e9394..4138f1c891 100644 --- a/src/Avalonia.Base/Media/Brush.cs +++ b/src/Avalonia.Base/Media/Brush.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media /// Describes how an area is painted. /// [TypeConverter(typeof(BrushConverter))] - public abstract class Brush : Animatable, IMutableBrush + public abstract class Brush : Animatable { /// /// Defines the property. @@ -92,9 +92,6 @@ namespace Avalonia.Media throw new FormatException($"Invalid brush string: '{s}'."); } - /// - public abstract IBrush ToImmutable(); - /// /// Marks a property as affecting the brush's visual representation. /// diff --git a/src/Avalonia.Base/Media/BrushExtensions.cs b/src/Avalonia.Base/Media/BrushExtensions.cs index 2fc8778a5e..d0208c187a 100644 --- a/src/Avalonia.Base/Media/BrushExtensions.cs +++ b/src/Avalonia.Base/Media/BrushExtensions.cs @@ -16,11 +16,11 @@ namespace Avalonia.Media /// The result of calling if the brush is mutable, /// otherwise . /// - public static IBrush ToImmutable(this IBrush brush) + public static IImmutableBrush ToImmutable(this IBrush brush) { _ = brush ?? throw new ArgumentNullException(nameof(brush)); - return (brush as IMutableBrush)?.ToImmutable() ?? brush; + return (brush as IMutableBrush)?.ToImmutable() ?? (IImmutableBrush)brush; } /// diff --git a/src/Avalonia.Base/Media/Brushes.cs b/src/Avalonia.Base/Media/Brushes.cs index 5957775f39..7ecba6df3e 100644 --- a/src/Avalonia.Base/Media/Brushes.cs +++ b/src/Avalonia.Base/Media/Brushes.cs @@ -8,706 +8,706 @@ namespace Avalonia.Media /// /// Gets an colored brush. /// - public static ISolidColorBrush AliceBlue => KnownColor.AliceBlue.ToBrush(); + public static IImmutableSolidColorBrush AliceBlue => KnownColor.AliceBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush AntiqueWhite => KnownColor.AntiqueWhite.ToBrush(); + public static IImmutableSolidColorBrush AntiqueWhite => KnownColor.AntiqueWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Aqua => KnownColor.Aqua.ToBrush(); + public static IImmutableSolidColorBrush Aqua => KnownColor.Aqua.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Aquamarine => KnownColor.Aquamarine.ToBrush(); + public static IImmutableSolidColorBrush Aquamarine => KnownColor.Aquamarine.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Azure => KnownColor.Azure.ToBrush(); + public static IImmutableSolidColorBrush Azure => KnownColor.Azure.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Beige => KnownColor.Beige.ToBrush(); + public static IImmutableSolidColorBrush Beige => KnownColor.Beige.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Bisque => KnownColor.Bisque.ToBrush(); + public static IImmutableSolidColorBrush Bisque => KnownColor.Bisque.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Black => KnownColor.Black.ToBrush(); + public static IImmutableSolidColorBrush Black => KnownColor.Black.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BlanchedAlmond => KnownColor.BlanchedAlmond.ToBrush(); + public static IImmutableSolidColorBrush BlanchedAlmond => KnownColor.BlanchedAlmond.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Blue => KnownColor.Blue.ToBrush(); + public static IImmutableSolidColorBrush Blue => KnownColor.Blue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BlueViolet => KnownColor.BlueViolet.ToBrush(); + public static IImmutableSolidColorBrush BlueViolet => KnownColor.BlueViolet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Brown => KnownColor.Brown.ToBrush(); + public static IImmutableSolidColorBrush Brown => KnownColor.Brown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BurlyWood => KnownColor.BurlyWood.ToBrush(); + public static IImmutableSolidColorBrush BurlyWood => KnownColor.BurlyWood.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush CadetBlue => KnownColor.CadetBlue.ToBrush(); + public static IImmutableSolidColorBrush CadetBlue => KnownColor.CadetBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Chartreuse => KnownColor.Chartreuse.ToBrush(); + public static IImmutableSolidColorBrush Chartreuse => KnownColor.Chartreuse.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Chocolate => KnownColor.Chocolate.ToBrush(); + public static IImmutableSolidColorBrush Chocolate => KnownColor.Chocolate.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Coral => KnownColor.Coral.ToBrush(); + public static IImmutableSolidColorBrush Coral => KnownColor.Coral.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush CornflowerBlue => KnownColor.CornflowerBlue.ToBrush(); + public static IImmutableSolidColorBrush CornflowerBlue => KnownColor.CornflowerBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Cornsilk => KnownColor.Cornsilk.ToBrush(); + public static IImmutableSolidColorBrush Cornsilk => KnownColor.Cornsilk.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Crimson => KnownColor.Crimson.ToBrush(); + public static IImmutableSolidColorBrush Crimson => KnownColor.Crimson.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Cyan => KnownColor.Cyan.ToBrush(); + public static IImmutableSolidColorBrush Cyan => KnownColor.Cyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkBlue => KnownColor.DarkBlue.ToBrush(); + public static IImmutableSolidColorBrush DarkBlue => KnownColor.DarkBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkCyan => KnownColor.DarkCyan.ToBrush(); + public static IImmutableSolidColorBrush DarkCyan => KnownColor.DarkCyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGoldenrod => KnownColor.DarkGoldenrod.ToBrush(); + public static IImmutableSolidColorBrush DarkGoldenrod => KnownColor.DarkGoldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGray => KnownColor.DarkGray.ToBrush(); + public static IImmutableSolidColorBrush DarkGray => KnownColor.DarkGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGreen => KnownColor.DarkGreen.ToBrush(); + public static IImmutableSolidColorBrush DarkGreen => KnownColor.DarkGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkKhaki => KnownColor.DarkKhaki.ToBrush(); + public static IImmutableSolidColorBrush DarkKhaki => KnownColor.DarkKhaki.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkMagenta => KnownColor.DarkMagenta.ToBrush(); + public static IImmutableSolidColorBrush DarkMagenta => KnownColor.DarkMagenta.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOliveGreen => KnownColor.DarkOliveGreen.ToBrush(); + public static IImmutableSolidColorBrush DarkOliveGreen => KnownColor.DarkOliveGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOrange => KnownColor.DarkOrange.ToBrush(); + public static IImmutableSolidColorBrush DarkOrange => KnownColor.DarkOrange.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOrchid => KnownColor.DarkOrchid.ToBrush(); + public static IImmutableSolidColorBrush DarkOrchid => KnownColor.DarkOrchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkRed => KnownColor.DarkRed.ToBrush(); + public static IImmutableSolidColorBrush DarkRed => KnownColor.DarkRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSalmon => KnownColor.DarkSalmon.ToBrush(); + public static IImmutableSolidColorBrush DarkSalmon => KnownColor.DarkSalmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSeaGreen => KnownColor.DarkSeaGreen.ToBrush(); + public static IImmutableSolidColorBrush DarkSeaGreen => KnownColor.DarkSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSlateBlue => KnownColor.DarkSlateBlue.ToBrush(); + public static IImmutableSolidColorBrush DarkSlateBlue => KnownColor.DarkSlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSlateGray => KnownColor.DarkSlateGray.ToBrush(); + public static IImmutableSolidColorBrush DarkSlateGray => KnownColor.DarkSlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkTurquoise => KnownColor.DarkTurquoise.ToBrush(); + public static IImmutableSolidColorBrush DarkTurquoise => KnownColor.DarkTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkViolet => KnownColor.DarkViolet.ToBrush(); + public static IImmutableSolidColorBrush DarkViolet => KnownColor.DarkViolet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DeepPink => KnownColor.DeepPink.ToBrush(); + public static IImmutableSolidColorBrush DeepPink => KnownColor.DeepPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DeepSkyBlue => KnownColor.DeepSkyBlue.ToBrush(); + public static IImmutableSolidColorBrush DeepSkyBlue => KnownColor.DeepSkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DimGray => KnownColor.DimGray.ToBrush(); + public static IImmutableSolidColorBrush DimGray => KnownColor.DimGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DodgerBlue => KnownColor.DodgerBlue.ToBrush(); + public static IImmutableSolidColorBrush DodgerBlue => KnownColor.DodgerBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Firebrick => KnownColor.Firebrick.ToBrush(); + public static IImmutableSolidColorBrush Firebrick => KnownColor.Firebrick.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush FloralWhite => KnownColor.FloralWhite.ToBrush(); + public static IImmutableSolidColorBrush FloralWhite => KnownColor.FloralWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush ForestGreen => KnownColor.ForestGreen.ToBrush(); + public static IImmutableSolidColorBrush ForestGreen => KnownColor.ForestGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Fuchsia => KnownColor.Fuchsia.ToBrush(); + public static IImmutableSolidColorBrush Fuchsia => KnownColor.Fuchsia.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gainsboro => KnownColor.Gainsboro.ToBrush(); + public static IImmutableSolidColorBrush Gainsboro => KnownColor.Gainsboro.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush GhostWhite => KnownColor.GhostWhite.ToBrush(); + public static IImmutableSolidColorBrush GhostWhite => KnownColor.GhostWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gold => KnownColor.Gold.ToBrush(); + public static IImmutableSolidColorBrush Gold => KnownColor.Gold.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Goldenrod => KnownColor.Goldenrod.ToBrush(); + public static IImmutableSolidColorBrush Goldenrod => KnownColor.Goldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gray => KnownColor.Gray.ToBrush(); + public static IImmutableSolidColorBrush Gray => KnownColor.Gray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Green => KnownColor.Green.ToBrush(); + public static IImmutableSolidColorBrush Green => KnownColor.Green.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush GreenYellow => KnownColor.GreenYellow.ToBrush(); + public static IImmutableSolidColorBrush GreenYellow => KnownColor.GreenYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Honeydew => KnownColor.Honeydew.ToBrush(); + public static IImmutableSolidColorBrush Honeydew => KnownColor.Honeydew.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush HotPink => KnownColor.HotPink.ToBrush(); + public static IImmutableSolidColorBrush HotPink => KnownColor.HotPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush IndianRed => KnownColor.IndianRed.ToBrush(); + public static IImmutableSolidColorBrush IndianRed => KnownColor.IndianRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Indigo => KnownColor.Indigo.ToBrush(); + public static IImmutableSolidColorBrush Indigo => KnownColor.Indigo.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Ivory => KnownColor.Ivory.ToBrush(); + public static IImmutableSolidColorBrush Ivory => KnownColor.Ivory.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Khaki => KnownColor.Khaki.ToBrush(); + public static IImmutableSolidColorBrush Khaki => KnownColor.Khaki.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Lavender => KnownColor.Lavender.ToBrush(); + public static IImmutableSolidColorBrush Lavender => KnownColor.Lavender.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LavenderBlush => KnownColor.LavenderBlush.ToBrush(); + public static IImmutableSolidColorBrush LavenderBlush => KnownColor.LavenderBlush.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LawnGreen => KnownColor.LawnGreen.ToBrush(); + public static IImmutableSolidColorBrush LawnGreen => KnownColor.LawnGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LemonChiffon => KnownColor.LemonChiffon.ToBrush(); + public static IImmutableSolidColorBrush LemonChiffon => KnownColor.LemonChiffon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightBlue => KnownColor.LightBlue.ToBrush(); + public static IImmutableSolidColorBrush LightBlue => KnownColor.LightBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightCoral => KnownColor.LightCoral.ToBrush(); + public static IImmutableSolidColorBrush LightCoral => KnownColor.LightCoral.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightCyan => KnownColor.LightCyan.ToBrush(); + public static IImmutableSolidColorBrush LightCyan => KnownColor.LightCyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGoldenrodYellow => KnownColor.LightGoldenrodYellow.ToBrush(); + public static IImmutableSolidColorBrush LightGoldenrodYellow => KnownColor.LightGoldenrodYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGray => KnownColor.LightGray.ToBrush(); + public static IImmutableSolidColorBrush LightGray => KnownColor.LightGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGreen => KnownColor.LightGreen.ToBrush(); + public static IImmutableSolidColorBrush LightGreen => KnownColor.LightGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightPink => KnownColor.LightPink.ToBrush(); + public static IImmutableSolidColorBrush LightPink => KnownColor.LightPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSalmon => KnownColor.LightSalmon.ToBrush(); + public static IImmutableSolidColorBrush LightSalmon => KnownColor.LightSalmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSeaGreen => KnownColor.LightSeaGreen.ToBrush(); + public static IImmutableSolidColorBrush LightSeaGreen => KnownColor.LightSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSkyBlue => KnownColor.LightSkyBlue.ToBrush(); + public static IImmutableSolidColorBrush LightSkyBlue => KnownColor.LightSkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSlateGray => KnownColor.LightSlateGray.ToBrush(); + public static IImmutableSolidColorBrush LightSlateGray => KnownColor.LightSlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSteelBlue => KnownColor.LightSteelBlue.ToBrush(); + public static IImmutableSolidColorBrush LightSteelBlue => KnownColor.LightSteelBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightYellow => KnownColor.LightYellow.ToBrush(); + public static IImmutableSolidColorBrush LightYellow => KnownColor.LightYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Lime => KnownColor.Lime.ToBrush(); + public static IImmutableSolidColorBrush Lime => KnownColor.Lime.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LimeGreen => KnownColor.LimeGreen.ToBrush(); + public static IImmutableSolidColorBrush LimeGreen => KnownColor.LimeGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Linen => KnownColor.Linen.ToBrush(); + public static IImmutableSolidColorBrush Linen => KnownColor.Linen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Magenta => KnownColor.Magenta.ToBrush(); + public static IImmutableSolidColorBrush Magenta => KnownColor.Magenta.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Maroon => KnownColor.Maroon.ToBrush(); + public static IImmutableSolidColorBrush Maroon => KnownColor.Maroon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumAquamarine => KnownColor.MediumAquamarine.ToBrush(); + public static IImmutableSolidColorBrush MediumAquamarine => KnownColor.MediumAquamarine.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumBlue => KnownColor.MediumBlue.ToBrush(); + public static IImmutableSolidColorBrush MediumBlue => KnownColor.MediumBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumOrchid => KnownColor.MediumOrchid.ToBrush(); + public static IImmutableSolidColorBrush MediumOrchid => KnownColor.MediumOrchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumPurple => KnownColor.MediumPurple.ToBrush(); + public static IImmutableSolidColorBrush MediumPurple => KnownColor.MediumPurple.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSeaGreen => KnownColor.MediumSeaGreen.ToBrush(); + public static IImmutableSolidColorBrush MediumSeaGreen => KnownColor.MediumSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSlateBlue => KnownColor.MediumSlateBlue.ToBrush(); + public static IImmutableSolidColorBrush MediumSlateBlue => KnownColor.MediumSlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSpringGreen => KnownColor.MediumSpringGreen.ToBrush(); + public static IImmutableSolidColorBrush MediumSpringGreen => KnownColor.MediumSpringGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumTurquoise => KnownColor.MediumTurquoise.ToBrush(); + public static IImmutableSolidColorBrush MediumTurquoise => KnownColor.MediumTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumVioletRed => KnownColor.MediumVioletRed.ToBrush(); + public static IImmutableSolidColorBrush MediumVioletRed => KnownColor.MediumVioletRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MidnightBlue => KnownColor.MidnightBlue.ToBrush(); + public static IImmutableSolidColorBrush MidnightBlue => KnownColor.MidnightBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MintCream => KnownColor.MintCream.ToBrush(); + public static IImmutableSolidColorBrush MintCream => KnownColor.MintCream.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MistyRose => KnownColor.MistyRose.ToBrush(); + public static IImmutableSolidColorBrush MistyRose => KnownColor.MistyRose.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Moccasin => KnownColor.Moccasin.ToBrush(); + public static IImmutableSolidColorBrush Moccasin => KnownColor.Moccasin.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush NavajoWhite => KnownColor.NavajoWhite.ToBrush(); + public static IImmutableSolidColorBrush NavajoWhite => KnownColor.NavajoWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Navy => KnownColor.Navy.ToBrush(); + public static IImmutableSolidColorBrush Navy => KnownColor.Navy.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OldLace => KnownColor.OldLace.ToBrush(); + public static IImmutableSolidColorBrush OldLace => KnownColor.OldLace.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Olive => KnownColor.Olive.ToBrush(); + public static IImmutableSolidColorBrush Olive => KnownColor.Olive.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OliveDrab => KnownColor.OliveDrab.ToBrush(); + public static IImmutableSolidColorBrush OliveDrab => KnownColor.OliveDrab.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Orange => KnownColor.Orange.ToBrush(); + public static IImmutableSolidColorBrush Orange => KnownColor.Orange.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OrangeRed => KnownColor.OrangeRed.ToBrush(); + public static IImmutableSolidColorBrush OrangeRed => KnownColor.OrangeRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Orchid => KnownColor.Orchid.ToBrush(); + public static IImmutableSolidColorBrush Orchid => KnownColor.Orchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleGoldenrod => KnownColor.PaleGoldenrod.ToBrush(); + public static IImmutableSolidColorBrush PaleGoldenrod => KnownColor.PaleGoldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleGreen => KnownColor.PaleGreen.ToBrush(); + public static IImmutableSolidColorBrush PaleGreen => KnownColor.PaleGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleTurquoise => KnownColor.PaleTurquoise.ToBrush(); + public static IImmutableSolidColorBrush PaleTurquoise => KnownColor.PaleTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleVioletRed => KnownColor.PaleVioletRed.ToBrush(); + public static IImmutableSolidColorBrush PaleVioletRed => KnownColor.PaleVioletRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PapayaWhip => KnownColor.PapayaWhip.ToBrush(); + public static IImmutableSolidColorBrush PapayaWhip => KnownColor.PapayaWhip.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PeachPuff => KnownColor.PeachPuff.ToBrush(); + public static IImmutableSolidColorBrush PeachPuff => KnownColor.PeachPuff.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Peru => KnownColor.Peru.ToBrush(); + public static IImmutableSolidColorBrush Peru => KnownColor.Peru.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Pink => KnownColor.Pink.ToBrush(); + public static IImmutableSolidColorBrush Pink => KnownColor.Pink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Plum => KnownColor.Plum.ToBrush(); + public static IImmutableSolidColorBrush Plum => KnownColor.Plum.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PowderBlue => KnownColor.PowderBlue.ToBrush(); + public static IImmutableSolidColorBrush PowderBlue => KnownColor.PowderBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Purple => KnownColor.Purple.ToBrush(); + public static IImmutableSolidColorBrush Purple => KnownColor.Purple.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Red => KnownColor.Red.ToBrush(); + public static IImmutableSolidColorBrush Red => KnownColor.Red.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush RosyBrown => KnownColor.RosyBrown.ToBrush(); + public static IImmutableSolidColorBrush RosyBrown => KnownColor.RosyBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush RoyalBlue => KnownColor.RoyalBlue.ToBrush(); + public static IImmutableSolidColorBrush RoyalBlue => KnownColor.RoyalBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SaddleBrown => KnownColor.SaddleBrown.ToBrush(); + public static IImmutableSolidColorBrush SaddleBrown => KnownColor.SaddleBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Salmon => KnownColor.Salmon.ToBrush(); + public static IImmutableSolidColorBrush Salmon => KnownColor.Salmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SandyBrown => KnownColor.SandyBrown.ToBrush(); + public static IImmutableSolidColorBrush SandyBrown => KnownColor.SandyBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SeaGreen => KnownColor.SeaGreen.ToBrush(); + public static IImmutableSolidColorBrush SeaGreen => KnownColor.SeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SeaShell => KnownColor.SeaShell.ToBrush(); + public static IImmutableSolidColorBrush SeaShell => KnownColor.SeaShell.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Sienna => KnownColor.Sienna.ToBrush(); + public static IImmutableSolidColorBrush Sienna => KnownColor.Sienna.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Silver => KnownColor.Silver.ToBrush(); + public static IImmutableSolidColorBrush Silver => KnownColor.Silver.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SkyBlue => KnownColor.SkyBlue.ToBrush(); + public static IImmutableSolidColorBrush SkyBlue => KnownColor.SkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SlateBlue => KnownColor.SlateBlue.ToBrush(); + public static IImmutableSolidColorBrush SlateBlue => KnownColor.SlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SlateGray => KnownColor.SlateGray.ToBrush(); + public static IImmutableSolidColorBrush SlateGray => KnownColor.SlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Snow => KnownColor.Snow.ToBrush(); + public static IImmutableSolidColorBrush Snow => KnownColor.Snow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SpringGreen => KnownColor.SpringGreen.ToBrush(); + public static IImmutableSolidColorBrush SpringGreen => KnownColor.SpringGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SteelBlue => KnownColor.SteelBlue.ToBrush(); + public static IImmutableSolidColorBrush SteelBlue => KnownColor.SteelBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Tan => KnownColor.Tan.ToBrush(); + public static IImmutableSolidColorBrush Tan => KnownColor.Tan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Teal => KnownColor.Teal.ToBrush(); + public static IImmutableSolidColorBrush Teal => KnownColor.Teal.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Thistle => KnownColor.Thistle.ToBrush(); + public static IImmutableSolidColorBrush Thistle => KnownColor.Thistle.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Tomato => KnownColor.Tomato.ToBrush(); + public static IImmutableSolidColorBrush Tomato => KnownColor.Tomato.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Transparent => KnownColor.Transparent.ToBrush(); + public static IImmutableSolidColorBrush Transparent => KnownColor.Transparent.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Turquoise => KnownColor.Turquoise.ToBrush(); + public static IImmutableSolidColorBrush Turquoise => KnownColor.Turquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Violet => KnownColor.Violet.ToBrush(); + public static IImmutableSolidColorBrush Violet => KnownColor.Violet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Wheat => KnownColor.Wheat.ToBrush(); + public static IImmutableSolidColorBrush Wheat => KnownColor.Wheat.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush White => KnownColor.White.ToBrush(); + public static IImmutableSolidColorBrush White => KnownColor.White.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush WhiteSmoke => KnownColor.WhiteSmoke.ToBrush(); + public static IImmutableSolidColorBrush WhiteSmoke => KnownColor.WhiteSmoke.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Yellow => KnownColor.Yellow.ToBrush(); + public static IImmutableSolidColorBrush Yellow => KnownColor.Yellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush YellowGreen => KnownColor.YellowGreen.ToBrush(); + public static IImmutableSolidColorBrush YellowGreen => KnownColor.YellowGreen.ToBrush(); } } diff --git a/src/Avalonia.Base/Media/ConicGradientBrush.cs b/src/Avalonia.Base/Media/ConicGradientBrush.cs index 4b50019ddc..bce7a5af19 100644 --- a/src/Avalonia.Base/Media/ConicGradientBrush.cs +++ b/src/Avalonia.Base/Media/ConicGradientBrush.cs @@ -47,7 +47,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + public override IImmutableBrush ToImmutable() { return new ImmutableConicGradientBrush(this); } diff --git a/src/Avalonia.Base/Media/DashStyle.cs b/src/Avalonia.Base/Media/DashStyle.cs index 3a30b2d32f..9c30b6f872 100644 --- a/src/Avalonia.Base/Media/DashStyle.cs +++ b/src/Avalonia.Base/Media/DashStyle.cs @@ -17,8 +17,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty> DashesProperty = - AvaloniaProperty.Register>(nameof(Dashes)); + public static readonly StyledProperty?> DashesProperty = + AvaloniaProperty.Register?>(nameof(Dashes)); /// /// Defines the property. @@ -83,7 +83,7 @@ namespace Avalonia.Media /// /// Gets or sets the length of alternating dashes and gaps. /// - public AvaloniaList Dashes + public AvaloniaList? Dashes { get => GetValue(DashesProperty); set => SetValue(DashesProperty, value); @@ -98,7 +98,7 @@ namespace Avalonia.Media set => SetValue(OffsetProperty, value); } - IReadOnlyList IDashStyle.Dashes => Dashes; + IReadOnlyList? IDashStyle.Dashes => Dashes; /// /// Raised when the dash style changes. diff --git a/src/Avalonia.Base/Media/GradientBrush.cs b/src/Avalonia.Base/Media/GradientBrush.cs index c84413ecbb..83cdaa1694 100644 --- a/src/Avalonia.Base/Media/GradientBrush.cs +++ b/src/Avalonia.Base/Media/GradientBrush.cs @@ -12,7 +12,7 @@ namespace Avalonia.Media /// /// Base class for brushes that draw with a gradient. /// - public abstract class GradientBrush : Brush, IGradientBrush + public abstract class GradientBrush : Brush, IGradientBrush, IMutableBrush { /// /// Defines the property. @@ -92,5 +92,7 @@ namespace Avalonia.Media { RaiseInvalidated(EventArgs.Empty); } + + public abstract IImmutableBrush ToImmutable(); } } diff --git a/src/Avalonia.Base/Media/IDashStyle.cs b/src/Avalonia.Base/Media/IDashStyle.cs index 7208216603..b988ad210a 100644 --- a/src/Avalonia.Base/Media/IDashStyle.cs +++ b/src/Avalonia.Base/Media/IDashStyle.cs @@ -12,7 +12,7 @@ namespace Avalonia.Media /// /// Gets or sets the length of alternating dashes and gaps. /// - IReadOnlyList Dashes { get; } + IReadOnlyList? Dashes { get; } /// /// Gets or sets how far in the dash sequence the stroke will start. diff --git a/src/Avalonia.Base/Media/IImmutableBrush.cs b/src/Avalonia.Base/Media/IImmutableBrush.cs new file mode 100644 index 0000000000..5781b0117a --- /dev/null +++ b/src/Avalonia.Base/Media/IImmutableBrush.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Media; + +/// +/// Represents an immutable brush which can be safely used with various threading contexts +/// +public interface IImmutableBrush : IBrush +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/IMutableBrush.cs b/src/Avalonia.Base/Media/IMutableBrush.cs index fef124ba36..f128aba4df 100644 --- a/src/Avalonia.Base/Media/IMutableBrush.cs +++ b/src/Avalonia.Base/Media/IMutableBrush.cs @@ -7,12 +7,12 @@ namespace Avalonia.Media /// Represents a mutable brush which can return an immutable clone of itself. /// [NotClientImplementable] - public interface IMutableBrush : IBrush, IAffectsRender + internal interface IMutableBrush : IBrush, IAffectsRender { /// /// Creates an immutable clone of the brush. /// /// The immutable clone. - IBrush ToImmutable(); + internal IImmutableBrush ToImmutable(); } } diff --git a/src/Avalonia.Base/Media/ISolidColorBrush.cs b/src/Avalonia.Base/Media/ISolidColorBrush.cs index 29e11210f1..f58768ef09 100644 --- a/src/Avalonia.Base/Media/ISolidColorBrush.cs +++ b/src/Avalonia.Base/Media/ISolidColorBrush.cs @@ -13,4 +13,13 @@ namespace Avalonia.Media /// Color Color { get; } } + + /// + /// Fills an area with a solid color. + /// + [NotClientImplementable] + public interface IImmutableSolidColorBrush : ISolidColorBrush, IImmutableBrush + { + + } } diff --git a/src/Avalonia.Base/Media/ImageBrush.cs b/src/Avalonia.Base/Media/ImageBrush.cs index 19dcf00901..2f2a0fb627 100644 --- a/src/Avalonia.Base/Media/ImageBrush.cs +++ b/src/Avalonia.Base/Media/ImageBrush.cs @@ -6,7 +6,7 @@ namespace Avalonia.Media /// /// Paints an area with an . /// - public class ImageBrush : TileBrush, IImageBrush + public class ImageBrush : TileBrush, IImageBrush, IMutableBrush { /// /// Defines the property. @@ -45,7 +45,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + public IImmutableBrush ToImmutable() { return new ImmutableImageBrush(this); } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs new file mode 100644 index 0000000000..1e1a73437d --- /dev/null +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -0,0 +1,373 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; +using Avalonia.Utilities; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; + +namespace Avalonia.Media +{ + public sealed class ImmediateDrawingContext : IDisposable, IOptionalFeatureProvider + { + private readonly bool _ownsImpl; + private int _currentLevel; + + private static ThreadSafeObjectPool> StateStackPool { get; } = + ThreadSafeObjectPool>.Default; + + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; + + private Stack? _states = StateStackPool.Get(); + + private Stack? _transformContainers = TransformStackPool.Get(); + + readonly struct TransformContainer + { + public readonly Matrix LocalTransform; + public readonly Matrix ContainerTransform; + + public TransformContainer(Matrix localTransform, Matrix containerTransform) + { + LocalTransform = localTransform; + ContainerTransform = containerTransform; + } + } + + internal ImmediateDrawingContext(IDrawingContextImpl impl, bool ownsImpl) + { + _ownsImpl = ownsImpl; + PlatformImpl = impl; + _currentContainerTransform = impl.Transform; + } + + public IDrawingContextImpl PlatformImpl { get; } + + private Matrix _currentTransform = Matrix.Identity; + + private Matrix _currentContainerTransform; + + /// + /// Gets the current transform of the drawing context. + /// + public Matrix CurrentTransform + { + get { return _currentTransform; } + private set + { + _currentTransform = value; + var transform = _currentTransform * _currentContainerTransform; + PlatformImpl.Transform = transform; + } + } + + /// + /// Draws an bitmap. + /// + /// The bitmap. + /// The rect in the output to draw to. + public void DrawBitmap(IBitmap source, Rect rect) + { + _ = source ?? throw new ArgumentNullException(nameof(source)); + DrawBitmap(source, new Rect(source.Size), rect); + } + + /// + /// Draws an image. + /// + /// The bitmap. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + public void DrawBitmap(IBitmap source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) + { + _ = source ?? throw new ArgumentNullException(nameof(source)); + PlatformImpl.DrawBitmap(source.PlatformImpl, 1, sourceRect, destRect, bitmapInterpolationMode); + } + + /// + /// Draws a line. + /// + /// The stroke pen. + /// The first point of the line. + /// The second point of the line. + public void DrawLine(ImmutablePen pen, Point p1, Point p2) + { + if (PenIsVisible(pen)) + { + PlatformImpl.DrawLine(pen, p1, p2); + } + } + + /// + /// Draws a rectangle with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The rectangle bounds. + /// The radius in the X dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Width/2 + /// + /// The radius in the Y dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Height/2 + /// + /// Box shadow effect parameters + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawRectangle(IImmutableBrush? brush, ImmutablePen? pen, Rect rect, double radiusX = 0, double radiusY = 0, + BoxShadows boxShadows = default) + { + if (brush == null && !PenIsVisible(pen)) + { + return; + } + + if (!MathUtilities.IsZero(radiusX)) + { + radiusX = Math.Min(radiusX, rect.Width / 2); + } + + if (!MathUtilities.IsZero(radiusY)) + { + radiusY = Math.Min(radiusY, rect.Height / 2); + } + + PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); + } + + /// + /// Draws the outline of a rectangle. + /// + /// The pen. + /// The rectangle bounds. + /// The corner radius. + public void DrawRectangle(ImmutablePen pen, Rect rect, float cornerRadius = 0.0f) + { + DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); + } + + /// + /// Draws an ellipse with the specified Brush and Pen. + /// + /// The brush used to fill the ellipse, or null for no fill. + /// The pen used to stroke the ellipse, or null for no stroke. + /// The location of the center of the ellipse. + /// The horizontal radius of the ellipse. + /// The vertical radius of the ellipse. + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawEllipse(IImmutableBrush? brush, ImmutablePen? pen, Point center, double radiusX, double radiusY) + { + if (brush == null && !PenIsVisible(pen)) + { + return; + } + + var originX = center.X - radiusX; + var originY = center.Y - radiusY; + var width = radiusX * 2; + var height = radiusY * 2; + + PlatformImpl.DrawEllipse(brush, pen, new Rect(originX, originY, width, height)); + } + + /// + /// Draws a glyph run. + /// + /// The foreground brush. + /// The glyph run. + public void DrawGlyphRun(IImmutableBrush foreground, GlyphRun glyphRun) + { + _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); + + PlatformImpl.DrawGlyphRun(foreground, glyphRun); + } + + /// + /// Draws a filled rectangle. + /// + /// The brush. + /// The rectangle bounds. + /// The corner radius. + public void FillRectangle(IImmutableBrush brush, Rect rect, float cornerRadius = 0.0f) + { + DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); + } + + public readonly record struct PushedState : IDisposable + { + private readonly int _level; + private readonly ImmediateDrawingContext _context; + private readonly Matrix _matrix; + private readonly PushedStateType _type; + + public enum PushedStateType + { + None, + Matrix, + Opacity, + Clip, + MatrixContainer, + GeometryClip, + OpacityMask, + } + + internal PushedState(ImmediateDrawingContext context, PushedStateType type, Matrix matrix = default(Matrix)) + { + if (context._states is null) + throw new ObjectDisposedException(nameof(ImmediateDrawingContext)); + + _context = context; + _type = type; + _matrix = matrix; + _level = context._currentLevel += 1; + context._states.Push(this); + } + + public void Dispose() + { + if (_type == PushedStateType.None) + return; + if (_context._states is null || _context._transformContainers is null) + throw new ObjectDisposedException(nameof(DrawingContext)); + if (_context._currentLevel != _level) + throw new InvalidOperationException("Wrong Push/Pop state order"); + _context._currentLevel--; + _context._states.Pop(); + if (_type == PushedStateType.Matrix) + _context.CurrentTransform = _matrix; + else if (_type == PushedStateType.Clip) + _context.PlatformImpl.PopClip(); + else if (_type == PushedStateType.Opacity) + _context.PlatformImpl.PopOpacity(); + else if (_type == PushedStateType.GeometryClip) + _context.PlatformImpl.PopGeometryClip(); + else if (_type == PushedStateType.OpacityMask) + _context.PlatformImpl.PopOpacityMask(); + else if (_type == PushedStateType.MatrixContainer) + { + var cont = _context._transformContainers.Pop(); + _context._currentContainerTransform = cont.ContainerTransform; + _context.CurrentTransform = cont.LocalTransform; + } + } + } + + + public PushedState PushClip(RoundedRect clip) + { + PlatformImpl.PushClip(clip); + return new PushedState(this, PushedState.PushedStateType.Clip); + } + + /// + /// Pushes a clip rectangle. + /// + /// The clip rectangle. + /// A disposable used to undo the clip rectangle. + public PushedState PushClip(Rect clip) + { + PlatformImpl.PushClip(clip); + return new PushedState(this, PushedState.PushedStateType.Clip); + } + + /// + /// Pushes an opacity value. + /// + /// The opacity. + /// A disposable used to undo the opacity. + public PushedState PushOpacity(double opacity) + //TODO: Eliminate platform-specific push opacity call + { + PlatformImpl.PushOpacity(opacity); + return new PushedState(this, PushedState.PushedStateType.Opacity); + } + + /// + /// Pushes an opacity mask. + /// + /// The opacity mask. + /// + /// The size of the brush's target area. TODO: Are we sure this is needed? + /// + /// A disposable to undo the opacity mask. + public PushedState PushOpacityMask(IImmutableBrush mask, Rect bounds) + { + PlatformImpl.PushOpacityMask(mask, bounds); + return new PushedState(this, PushedState.PushedStateType.OpacityMask); + } + + /// + /// Pushes a matrix post-transformation. + /// + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform * matrix); + + /// + /// Pushes a matrix pre-transformation. + /// + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix * CurrentTransform); + + /// + /// Sets the current matrix transformation. + /// + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushSetTransform(Matrix matrix) + { + var oldMatrix = CurrentTransform; + CurrentTransform = matrix; + + return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); + } + + /// + /// Pushes a new transform context. + /// + /// A disposable used to undo the transformation. + public PushedState PushTransformContainer() + { + if (_transformContainers is null) + throw new ObjectDisposedException(nameof(DrawingContext)); + _transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform)); + _currentContainerTransform = CurrentTransform * _currentContainerTransform; + _currentTransform = Matrix.Identity; + return new PushedState(this, PushedState.PushedStateType.MatrixContainer); + } + + /// + /// Disposes of any resources held by the . + /// + public void Dispose() + { + if (_states is null || _transformContainers is null) + throw new ObjectDisposedException(nameof(DrawingContext)); + while (_states.Count != 0) + _states.Peek().Dispose(); + StateStackPool.Return(_states); + _states = null; + if (_transformContainers.Count != 0) + throw new InvalidOperationException("Transform container stack is non-empty"); + TransformStackPool.Return(_transformContainers); + _transformContainers = null; + if (_ownsImpl) + PlatformImpl.Dispose(); + } + + private static bool PenIsVisible(IPen? pen) + { + return pen?.Brush != null && pen.Thickness > 0; + } + + public object? TryGetFeature(Type type) => PlatformImpl.GetFeature(type); + } +} diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs index 82485c13b0..1f53f06955 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs @@ -17,7 +17,7 @@ namespace Avalonia.Media.Immutable /// /// The dashes collection. /// The dash sequence offset. - public ImmutableDashStyle(IEnumerable dashes, double offset) + public ImmutableDashStyle(IEnumerable? dashes, double offset) { _dashes = dashes?.ToArray() ?? Array.Empty(); Offset = offset; @@ -69,7 +69,7 @@ namespace Avalonia.Media.Immutable return hashCode; } - private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList right) + private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList? right) { if (ReferenceEquals(left, right)) { diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableGradientBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableGradientBrush.cs index 1e95acbf22..c86d86d20a 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableGradientBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableGradientBrush.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Immutable /// /// A brush that draws with a gradient. /// - public abstract class ImmutableGradientBrush : IGradientBrush + public abstract class ImmutableGradientBrush : IGradientBrush, IImmutableBrush { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Base/Media/Immutable/ImmutablePen.cs index 8a53eaf7b8..ef4468fccf 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutablePen.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutablePen.cs @@ -38,15 +38,13 @@ namespace Avalonia.Media.Immutable /// The line join. /// The miter limit. public ImmutablePen( - IBrush? brush, + IImmutableBrush? brush, double thickness = 1.0, ImmutableDashStyle? dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) { - Debug.Assert(!(brush is IMutableBrush)); - Brush = brush; Thickness = thickness; LineCap = lineCap; diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableSolidColorBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableSolidColorBrush.cs index 6755dfd236..4e623f02c5 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableSolidColorBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableSolidColorBrush.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Immutable /// /// Fills an area with a solid color. /// - public class ImmutableSolidColorBrush : ISolidColorBrush, IEquatable + public class ImmutableSolidColorBrush : IImmutableSolidColorBrush, IEquatable { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs index 6df27872fc..1ee52365e0 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Immutable /// /// A brush which displays a repeating image. /// - public abstract class ImmutableTileBrush : ITileBrush + public abstract class ImmutableTileBrush : ITileBrush, IImmutableBrush { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Media/KnownColors.cs b/src/Avalonia.Base/Media/KnownColors.cs index ae2fca5a60..64cf1ef2f1 100644 --- a/src/Avalonia.Base/Media/KnownColors.cs +++ b/src/Avalonia.Base/Media/KnownColors.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media private static readonly IReadOnlyDictionary _knownColorNames; private static readonly IReadOnlyDictionary _knownColors; #if !BUILDTASK - private static readonly Dictionary _knownBrushes; + private static readonly Dictionary _knownBrushes; #endif [GenerateEnumValueDictionary()] @@ -39,7 +39,7 @@ namespace Avalonia.Media _knownColors = knownColors; #if !BUILDTASK - _knownBrushes = new Dictionary(); + _knownBrushes = new (); #endif } @@ -72,7 +72,7 @@ namespace Avalonia.Media } #if !BUILDTASK - public static ISolidColorBrush ToBrush(this KnownColor color) + public static IImmutableSolidColorBrush ToBrush(this KnownColor color) { lock (_knownBrushes) { diff --git a/src/Avalonia.Base/Media/LinearGradientBrush.cs b/src/Avalonia.Base/Media/LinearGradientBrush.cs index a51adf0949..9b9ce5e4b7 100644 --- a/src/Avalonia.Base/Media/LinearGradientBrush.cs +++ b/src/Avalonia.Base/Media/LinearGradientBrush.cs @@ -47,7 +47,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + public override IImmutableBrush ToImmutable() { return new ImmutableLinearGradientBrush(this); } diff --git a/src/Avalonia.Base/Media/RadialGradientBrush.cs b/src/Avalonia.Base/Media/RadialGradientBrush.cs index 16c367c0d0..f803051676 100644 --- a/src/Avalonia.Base/Media/RadialGradientBrush.cs +++ b/src/Avalonia.Base/Media/RadialGradientBrush.cs @@ -67,7 +67,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + public override IImmutableBrush ToImmutable() { return new ImmutableRadialGradientBrush(this); } diff --git a/src/Avalonia.Base/Media/SolidColorBrush.cs b/src/Avalonia.Base/Media/SolidColorBrush.cs index 962819a1a1..d8e25fe748 100644 --- a/src/Avalonia.Base/Media/SolidColorBrush.cs +++ b/src/Avalonia.Base/Media/SolidColorBrush.cs @@ -6,7 +6,7 @@ namespace Avalonia.Media /// /// Fills an area with a solid color. /// - public class SolidColorBrush : Brush, ISolidColorBrush + public class SolidColorBrush : Brush, ISolidColorBrush, IMutableBrush { /// /// Defines the property. @@ -80,7 +80,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + public IImmutableBrush ToImmutable() { return new ImmutableSolidColorBrush(this); } diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 8dce79cf11..1261d233ac 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -6,7 +6,7 @@ namespace Avalonia.Media /// /// Paints an area with an . /// - public class VisualBrush : TileBrush, IVisualBrush + public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush { /// /// Defines the property. @@ -45,7 +45,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + IImmutableBrush IMutableBrush.ToImmutable() { return new ImmutableVisualBrush(this); } diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index ef14211902..b5ac5c817d 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reactive.Disposables; using Avalonia.Data; +using Avalonia.Threading; namespace Avalonia.PropertyStore { @@ -116,26 +117,42 @@ namespace Avalonia.PropertyStore private void SetValue(BindingValue value) { - if (Frame.Owner is null) - return; + static void Execute(BindingEntryBase instance, BindingValue value) + { + if (instance.Frame.Owner is null) + return; - LoggingUtils.LogIfNecessary(Frame.Owner.Owner, Property, value); + LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value); - if (value.HasValue) - { - if (!_hasValue || !EqualityComparer.Default.Equals(_value, value.Value)) + if (value.HasValue) + { + if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, value.Value)) + { + instance._value = value.Value; + instance._hasValue = true; + if (instance._subscription is not null && instance._subscription != s_creatingQuiet) + instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); + } + } + else if (value.Type != BindingValueType.DoNothing) { - _value = value.Value; - _hasValue = true; - if (_subscription is not null && _subscription != s_creatingQuiet) - Frame.Owner?.OnBindingValueChanged(this, Frame.Priority); + instance.ClearValue(); + if (instance._subscription is not null && instance._subscription != s_creatingQuiet) + instance.Frame.Owner?.OnBindingValueCleared(instance.Property, instance.Frame.Priority); } } - else if (value.Type != BindingValueType.DoNothing) + + if (Dispatcher.UIThread.CheckAccess()) { - ClearValue(); - if (_subscription is not null && _subscription != s_creatingQuiet) - Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); + Execute(this, value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = this; + var newValue = value; + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index 4dca6c0100..8acb885604 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Data; +using Avalonia.Threading; namespace Avalonia.PropertyStore { @@ -40,20 +41,56 @@ namespace Avalonia.PropertyStore public void OnNext(T value) { - if (Property.ValidateValue?.Invoke(value) != false) - _owner.SetValue(Property, value, BindingPriority.LocalValue); + static void Execute(ValueStore owner, StyledPropertyBase property, T value) + { + if (property.ValidateValue?.Invoke(value) != false) + owner.SetValue(property, value, BindingPriority.LocalValue); + else + owner.ClearLocalValue(property); + } + + if (Dispatcher.UIThread.CheckAccess()) + { + Execute(_owner, Property, value); + } else - _owner.ClearLocalValue(Property); + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = _owner; + var property = Property; + var newValue = value; + Dispatcher.UIThread.Post(() => Execute(instance, property, newValue)); + } } public void OnNext(BindingValue value) { - LoggingUtils.LogIfNecessary(_owner.Owner, Property, value); + static void Execute(LocalValueBindingObserver instance, BindingValue value) + { + var owner = instance._owner; + var property = instance.Property; + + LoggingUtils.LogIfNecessary(owner.Owner, property, value); - if (value.HasValue) - _owner.SetValue(Property, value.Value, BindingPriority.LocalValue); - else if (value.Type != BindingValueType.DataValidationError) - _owner.ClearLocalValue(Property); + if (value.HasValue) + owner.SetValue(property, value.Value, BindingPriority.LocalValue); + else if (value.Type != BindingValueType.DataValidationError) + owner.ClearLocalValue(property); + } + + if (Dispatcher.UIThread.CheckAccess()) + { + Execute(this, value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = this; + var newValue = value; + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); + } } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs index 099e997d38..7c529591b6 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs @@ -1,5 +1,7 @@ using System; +using System.Security.Cryptography; using Avalonia.Data; +using Avalonia.Threading; namespace Avalonia.PropertyStore { @@ -34,28 +36,47 @@ namespace Avalonia.PropertyStore public void OnNext(object? value) { - if (value is BindingNotification n) + static void Execute(LocalValueUntypedBindingObserver instance, object? value) { - value = n.Value; - LoggingUtils.LogIfNecessary(_owner.Owner, Property, n); - } + var owner = instance._owner; + var property = instance.Property; - if (value == AvaloniaProperty.UnsetValue) - { - _owner.ClearLocalValue(Property); - } - else if (value == BindingOperations.DoNothing) - { - // Do nothing! + if (value is BindingNotification n) + { + value = n.Value; + LoggingUtils.LogIfNecessary(owner.Owner, property, n); + } + + if (value == AvaloniaProperty.UnsetValue) + { + owner.ClearLocalValue(property); + } + else if (value == BindingOperations.DoNothing) + { + // Do nothing! + } + else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue)) + { + owner.SetValue(property, typedValue, BindingPriority.LocalValue); + } + else + { + owner.ClearLocalValue(property); + LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value); + } } - else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) + + if (Dispatcher.UIThread.CheckAccess()) { - _owner.SetValue(Property, typedValue, BindingPriority.LocalValue); + Execute(this, value); } - else + else if (value != BindingOperations.DoNothing) { - _owner.ClearLocalValue(Property); - LoggingUtils.LogInvalidValue(_owner.Owner, Property, typeof(T), value); + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = this; + var newValue = value; + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs index 889c1d34c8..80b6bb9d2a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs @@ -80,4 +80,6 @@ internal abstract class AnimationInstanceBase : IAnimationInstance _invalidated = true; TargetObject.NotifyAnimatedValueChanged(Property); } + + public void OnTick() => Invalidate(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs index 8ec4ec19bc..b14d2c0b19 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -6,7 +6,7 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { - internal interface IAnimationInstance + internal interface IAnimationInstance : IServerClockItem { ServerObject TargetObject { get; } ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs new file mode 100644 index 0000000000..f816231781 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition; + +public class CompositionCustomVisual : CompositionContainerVisual +{ + private List? _messages; + + internal CompositionCustomVisual(Compositor compositor, CompositionCustomVisualHandler handler) + : base(compositor, new ServerCompositionCustomVisual(compositor.Server, handler)) + { + + } + + public void SendHandlerMessage(object message) + { + (_messages ??= new()).Add(message); + RegisterForSerialization(); + } + + private protected override void SerializeChangesCore(BatchStreamWriter writer) + { + base.SerializeChangesCore(writer); + if (_messages == null || _messages.Count == 0) + writer.Write(0); + else + { + writer.Write(_messages.Count); + foreach (var m in _messages) + writer.WriteObject(m); + _messages.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs new file mode 100644 index 0000000000..b7ed6fe612 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Numerics; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition; + +public abstract class CompositionCustomVisualHandler +{ + private ServerCompositionCustomVisual? _host; + + public virtual void OnMessage(object message) + { + + } + + public virtual void OnAnimationFrameUpdate() + { + + } + + public abstract void OnRender(ImmediateDrawingContext drawingContext); + + void VerifyAccess() + { + if (_host == null) + throw new InvalidOperationException("Object is not yet attached to the compositor"); + _host.Compositor.VerifyAccess(); + } + + protected Vector2 EffectiveSize + { + get + { + VerifyAccess(); + return _host!.Size; + } + } + + protected TimeSpan CompositionNow + { + get + { + VerifyAccess(); + return _host!.Compositor.ServerNow; + } + } + + public virtual Rect GetRenderBounds() => + new(0, 0, EffectiveSize.X, EffectiveSize.Y); + + internal void Attach(ServerCompositionCustomVisual visual) => _host = visual; + + protected void Invalidate() + { + VerifyAccess(); + _host!.HandlerInvalidate(); + } + + protected void RegisterForNextAnimationFrameUpdate() + { + VerifyAccess(); + _host!.HandlerRegisterForNextAnimationFrameUpdate(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 4ce87b67a5..2b1b3f461f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -33,4 +33,6 @@ public partial class Compositor public CompositionSolidColorVisual CreateSolidColorVisual() => new(this, new ServerCompositionSolidColorVisual(Server)); + + public CompositionCustomVisual CreateCustomVisual(CompositionCustomVisualHandler handler) => new(this, handler); } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/IServerClockItem.cs b/src/Avalonia.Base/Rendering/Composition/Server/IServerClockItem.cs new file mode 100644 index 0000000000..d13f1158c8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/IServerClockItem.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Rendering.Composition.Server; + +internal interface IServerClockItem +{ + void OnTick(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 0fc5cecef3..b172430fbb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -87,8 +87,6 @@ namespace Avalonia.Rendering.Composition.Server } _renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); - - Compositor.UpdateServerTime(); if(_dirtyRect.IsDefault && !_redrawRequested) return; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index 387998f8d6..0e15cbd54b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -216,13 +216,29 @@ namespace Avalonia.Rendering.Composition.Server partial void OnRootChanging() { if (Root != null) + { Root.RemoveVisual(this); + OnDetachedFromRoot(Root); + } + } + + protected virtual void OnDetachedFromRoot(ServerCompositionTarget target) + { + } partial void OnRootChanged() { if (Root != null) + { Root.AddVisual(this); + OnAttachedToRoot(Root); + } + } + + protected virtual void OnAttachedToRoot(ServerCompositionTarget target) + { + } protected override void ValuesInvalidated() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 041a3dd6af..e7405995f5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading; using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; @@ -26,11 +27,12 @@ namespace Avalonia.Rendering.Composition.Server public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } private List _activeTargets = new(); - private HashSet _activeAnimations = new(); - private List _animationsToUpdate = new(); + private HashSet _clockItems = new(); + private List _clockItemsToUpdate = new(); internal BatchStreamObjectPool BatchObjectPool; internal BatchStreamMemoryPool BatchMemoryPool; private object _lock = new object(); + private Thread? _safeThread; public PlatformRenderInterfaceContextManager RenderInterface { get; } internal static readonly object RenderThreadJobsStartMarker = new(); internal static readonly object RenderThreadJobsEndMarker = new(); @@ -129,22 +131,31 @@ namespace Avalonia.Rendering.Composition.Server { lock (_lock) { - RenderCore(); + try + { + _safeThread = Thread.CurrentThread; + RenderCore(); + } + finally + { + _safeThread = null; + } } } private void RenderCore() { + UpdateServerTime(); ApplyPendingBatches(); CompletePendingBatches(); - foreach(var animation in _activeAnimations) - _animationsToUpdate.Add(animation); - - foreach(var animation in _animationsToUpdate) - animation.Invalidate(); + foreach(var animation in _clockItems) + _clockItemsToUpdate.Add(animation); + + foreach (var animation in _clockItemsToUpdate) + animation.OnTick(); - _animationsToUpdate.Clear(); + _clockItemsToUpdate.Clear(); try { @@ -168,16 +179,23 @@ namespace Avalonia.Rendering.Composition.Server _activeTargets.Remove(target); } - public void AddToClock(IAnimationInstance animationInstance) => - _activeAnimations.Add(animationInstance); + public void AddToClock(IServerClockItem item) => + _clockItems.Add(item); - public void RemoveFromClock(IAnimationInstance animationInstance) => - _activeAnimations.Remove(animationInstance); + public void RemoveFromClock(IServerClockItem item) => + _clockItems.Remove(item); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { using (RenderInterface.EnsureCurrent()) return RenderInterface.CreateRenderTarget(surfaces); } + + public bool CheckAccess() => _safeThread == Thread.CurrentThread; + public void VerifyAccess() + { + if (!CheckAccess()) + throw new InvalidOperationException("This object can be only accessed under compositor lock"); + } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs new file mode 100644 index 0000000000..227df87a87 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs @@ -0,0 +1,82 @@ +using System; +using System.Numerics; +using Avalonia.Logging; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionCustomVisual : ServerCompositionContainerVisual, IServerClockItem +{ + private readonly CompositionCustomVisualHandler _handler; + private bool _wantsNextAnimationFrameAfterTick; + internal ServerCompositionCustomVisual(ServerCompositor compositor, CompositionCustomVisualHandler handler) : base(compositor) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + _handler.Attach(this); + } + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + { + base.DeserializeChangesCore(reader, commitedAt); + var count = reader.Read(); + for (var c = 0; c < count; c++) + { + try + { + _handler.OnMessage(reader.ReadObject()!); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual) + ?.Log(_handler, $"Exception in {_handler.GetType().Name}.{nameof(CompositionCustomVisualHandler.OnMessage)} {{0}}", e); + } + } + } + + public void OnTick() + { + _wantsNextAnimationFrameAfterTick = false; + _handler.OnAnimationFrameUpdate(); + if (!_wantsNextAnimationFrameAfterTick) + Compositor.RemoveFromClock(this); + } + + public override Rect OwnContentBounds => _handler.GetRenderBounds(); + + protected override void OnAttachedToRoot(ServerCompositionTarget target) + { + if (_wantsNextAnimationFrameAfterTick) + Compositor.AddToClock(this); + base.OnAttachedToRoot(target); + } + + protected override void OnDetachedFromRoot(ServerCompositionTarget target) + { + Compositor.RemoveFromClock(this); + base.OnDetachedFromRoot(target); + } + + internal void HandlerInvalidate() => ValuesInvalidated(); + + internal void HandlerRegisterForNextAnimationFrameUpdate() + { + _wantsNextAnimationFrameAfterTick = true; + if (Root != null) + Compositor.AddToClock(this); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + { + using var context = new ImmediateDrawingContext(canvas, false); + try + { + _handler.OnRender(context); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual) + ?.Log(_handler, $"Exception in {_handler.GetType().Name}.{nameof(CompositionCustomVisualHandler.OnRender)} {{0}}", e); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index 6981dc39e3..93e9f4d740 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -69,7 +69,7 @@ namespace Avalonia.Rendering.Composition { if (item.Parent != null) throw new InvalidOperationException("Visual already has a parent"); - item.Parent = item; + item.Parent = _owner; } } } diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index e3dc4fbb75..ddaea01b05 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -540,6 +540,9 @@ namespace Avalonia OnDetachedFromVisualTree(e); if (CompositionVisual != null) { + if (ChildCompositionVisual != null) + CompositionVisual.Children.Remove(ChildCompositionVisual); + CompositionVisual.DrawList = null; CompositionVisual = null; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index d417c87746..2b715d6a9f 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -50,10 +50,13 @@ namespace Avalonia.Controls InheritsWidth = true; } - internal DataGrid OwningGrid + /// + /// Gets the control that contains this column. + /// + protected internal DataGrid OwningGrid { get; - set; + internal set; } internal int Index diff --git a/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs b/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs index 6144679b60..c823ee1b9c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs @@ -22,14 +22,18 @@ namespace Avalonia.Controls return DefaultValueConverter.Instance.Convert(value, targetType, parameter, culture); } - // This suppresses a warning saying that we should use String.IsNullOrEmpty instead of a string - // comparison, but in this case we want to explicitly check for Empty and not Null. + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { if (targetType != null && targetType.IsNullableType()) { var strValue = value as string; - if (string.IsNullOrEmpty(strValue)) + + // This suppresses a warning saying that we should use String.IsNullOrEmpty instead of a string + // comparison, but in this case we want to explicitly check for Empty and not Null. +#pragma warning disable CA1820 + if (strValue == string.Empty) +#pragma warning restore CA1820 { return null; } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index c59458311c..757db96799 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -83,12 +83,12 @@ namespace Avalonia.Controls.ApplicationLifetimes public void Shutdown(int exitCode = 0) { - DoShutdown(new ShutdownRequestedEventArgs(), true, exitCode); + DoShutdown(new ShutdownRequestedEventArgs(), true, true, exitCode); } public bool TryShutdown(int exitCode = 0) { - return DoShutdown(new ShutdownRequestedEventArgs(), false, exitCode); + return DoShutdown(new ShutdownRequestedEventArgs(), true, false, exitCode); } public int Start(string[] args) @@ -134,7 +134,11 @@ namespace Avalonia.Controls.ApplicationLifetimes _activeLifetime = null; } - private bool DoShutdown(ShutdownRequestedEventArgs e, bool force = false, int exitCode = 0) + private bool DoShutdown( + ShutdownRequestedEventArgs e, + bool isProgrammatic, + bool force = false, + int exitCode = 0) { if (!force) { @@ -159,7 +163,7 @@ namespace Avalonia.Controls.ApplicationLifetimes { if (w.Owner is null) { - w.Close(); + w.CloseCore(WindowCloseReason.ApplicationShutdown, isProgrammatic); } } @@ -183,7 +187,7 @@ namespace Avalonia.Controls.ApplicationLifetimes return true; } - private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e) => DoShutdown(e); + private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e) => DoShutdown(e, false); } public class ClassicDesktopStyleApplicationLifetimeOptions diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs index 8b3e875e5a..ac7a45a547 100644 --- a/src/Avalonia.Controls/NativeMenuBar.cs +++ b/src/Avalonia.Controls/NativeMenuBar.cs @@ -23,15 +23,6 @@ namespace Avalonia.Controls }); } - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenu))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenuItem))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenuItemBase))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenuItemSeparator))] - public NativeMenuBar() - { - - } - public static void SetEnableMenuItemClickForwarding(MenuItem menuItem, bool enable) { menuItem.SetValue(EnableMenuItemClickForwardingProperty, enable); diff --git a/src/Avalonia.Controls/NativeMenuItemSeparator.cs b/src/Avalonia.Controls/NativeMenuItemSeparator.cs index 97278130b1..f55d714884 100644 --- a/src/Avalonia.Controls/NativeMenuItemSeparator.cs +++ b/src/Avalonia.Controls/NativeMenuItemSeparator.cs @@ -1,7 +1,10 @@ namespace Avalonia.Controls { - public class NativeMenuItemSeparator : NativeMenuItemBase + public class NativeMenuItemSeparator : NativeMenuItem { - + public NativeMenuItemSeparator() + { + Header = "-"; + } } } diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index af9392d440..8d9d8e0e7b 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -68,7 +68,7 @@ namespace Avalonia.Platform /// Gets or sets a method called before the underlying implementation is destroyed. /// Return true to prevent the underlying implementation from closing. /// - Func Closing { get; set; } + Func Closing { get; set; } /// /// Gets a value to indicate if the platform was able to extend client area to non-client area. diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 798caf0b15..480a8ee7a7 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -109,7 +109,7 @@ namespace Avalonia.Controls.Presenters static TextPresenter() { - AffectsRender(CaretBrushProperty, SelectionBrushProperty); + AffectsRender(CaretBrushProperty, SelectionBrushProperty, TextElement.ForegroundProperty); } public TextPresenter() diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index b0911403cc..88f3b520ce 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -430,14 +430,14 @@ namespace Avalonia.Controls /// /// Fired before a window is closed. /// - public event EventHandler? Closing; + public event EventHandler? Closing; /// /// Closes the window. /// public void Close() { - Close(false); + CloseCore(WindowCloseReason.WindowClosing, true); } /// @@ -453,16 +453,16 @@ namespace Avalonia.Controls public void Close(object dialogResult) { _dialogResult = dialogResult; - Close(false); + CloseCore(WindowCloseReason.WindowClosing, true); } - internal void Close(bool ignoreCancel) + internal void CloseCore(WindowCloseReason reason, bool isProgrammatic) { bool close = true; try { - if (!ignoreCancel && ShouldCancelClose()) + if (ShouldCancelClose(new WindowClosingEventArgs(reason, isProgrammatic))) { close = false; } @@ -480,9 +480,10 @@ namespace Avalonia.Controls /// Handles a closing notification from . /// true if closing is cancelled. Otherwise false. /// - protected virtual bool HandleClosing() + /// The reason the window is closing. + private protected virtual bool HandleClosing(WindowCloseReason reason) { - if (!ShouldCancelClose()) + if (!ShouldCancelClose(new WindowClosingEventArgs(reason, false))) { CloseInternal(); return false; @@ -510,20 +511,22 @@ namespace Avalonia.Controls _showingAsDialog = false; } - private bool ShouldCancelClose(CancelEventArgs? args = null) + private bool ShouldCancelClose(WindowClosingEventArgs args) { - if (args is null) - { - args = new CancelEventArgs(); - } - bool canClose = true; - foreach (var (child, _) in _children.ToArray()) + if (_children.Count > 0) { - if (child.ShouldCancelClose(args)) + var childArgs = args.CloseReason == WindowCloseReason.WindowClosing ? + new WindowClosingEventArgs(WindowCloseReason.OwnerWindowClosing, args.IsProgrammatic) : + args; + + foreach (var (child, _) in _children.ToArray()) { - canClose = false; + if (child.ShouldCancelClose(childArgs)) + { + canClose = false; + } } } @@ -1033,7 +1036,7 @@ namespace Avalonia.Controls /// overridden method must call on the base class if the /// event needs to be raised. /// - protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); + protected virtual void OnClosing(WindowClosingEventArgs e) => Closing?.Invoke(this, e); protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Avalonia.Controls/WindowClosingEventArgs.cs b/src/Avalonia.Controls/WindowClosingEventArgs.cs new file mode 100644 index 0000000000..7cfae2d005 --- /dev/null +++ b/src/Avalonia.Controls/WindowClosingEventArgs.cs @@ -0,0 +1,57 @@ +using System.ComponentModel; + +namespace Avalonia.Controls +{ + /// + /// Specifies the reason that a window was closed. + /// + public enum WindowCloseReason + { + /// + /// The cause of the closure was not provided by the underlying platform. + /// + Undefined, + + /// + /// The window itself was requested to close. + /// + WindowClosing, + + /// + /// The window is closing due to a parent/owner window closing. + /// + OwnerWindowClosing, + + /// + /// The window is closing due to the application shutting down. + /// + ApplicationShutdown, + + /// + /// The window is closing due to the operating system shutting down. + /// + OSShutdown, + } + + /// + /// Provides data for the event. + /// + public class WindowClosingEventArgs : CancelEventArgs + { + internal WindowClosingEventArgs(WindowCloseReason reason, bool isProgrammatic) + { + CloseReason = reason; + IsProgrammatic = isProgrammatic; + } + + /// + /// Gets a value that indicates why the window is being closed. + /// + public WindowCloseReason CloseReason { get; } + + /// + /// Gets a value indicating whether the window is being closed programmatically. + /// + public bool IsProgrammatic { get; } + } +} diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 037c5e0c71..020e09526e 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -42,7 +42,7 @@ namespace Avalonia.DesignerSupport.Remote public Action PositionChanged { get; set; } public Action Deactivated { get; set; } public Action Activated { get; set; } - public Func Closing { get; set; } + public Func Closing { get; set; } public IPlatformHandle Handle { get; } public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 80a4c7d897..94679e8ade 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -32,7 +32,7 @@ namespace Avalonia.DesignerSupport.Remote public Action Paint { get; set; } public Action Resized { get; set; } public Action ScalingChanged { get; set; } - public Func Closing { get; set; } + public Func Closing { get; set; } public Action Closed { get; set; } public Action LostFocus { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 725fab1eaa..8eafce208b 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -175,7 +175,7 @@ namespace Avalonia.Headless } - public Func Closing { get; set; } + public Func Closing { get; set; } class FramebufferProxy : ILockedFramebuffer { diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 2201503168..880a385744 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -48,7 +48,7 @@ namespace Avalonia.Native { if (_parent.Closing != null) { - return _parent.Closing().AsComBool(); + return _parent.Closing(WindowCloseReason.WindowClosing).AsComBool(); } return true.AsComBool(); @@ -207,7 +207,7 @@ namespace Avalonia.Native // NO OP on OSX } - public Func Closing { get; set; } + public Func Closing { get; set; } public ITopLevelNativeMenuExporter NativeMenuExporter { get; } diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml index 5c48c297b8..d5c95ab46c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml @@ -9,17 +9,16 @@ IsVisible="{Binding !$parent[TopLevel].(NativeMenu.IsNativeMenuExported)}" Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}"> - - diff --git a/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml index ba1b35e2ee..945622f22f 100644 --- a/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml @@ -9,17 +9,16 @@ - - diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 890b49344c..810a806c8a 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -358,7 +358,7 @@ namespace Avalonia.X11 public Action ScalingChanged { get; set; } public Action Deactivated { get; set; } public Action Activated { get; set; } - public Func Closing { get; set; } + public Func Closing { get; set; } public Action WindowStateChanged { get; set; } public Action TransparencyLevelChanged @@ -546,7 +546,7 @@ namespace Avalonia.X11 { if (ev.ClientMessageEvent.ptr1 == _x11.Atoms.WM_DELETE_WINDOW) { - if (Closing?.Invoke() != true) + if (Closing?.Invoke(WindowCloseReason.WindowClosing) != true) Dispose(); } else if (ev.ClientMessageEvent.ptr1 == _x11.Atoms._NET_WM_SYNC_REQUEST) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 848da1bfb6..a7bec62366 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -1137,11 +1137,14 @@ namespace Avalonia.Skia if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0) { var srcDashes = pen.DashStyle.Dashes; - var dashesArray = new float[srcDashes.Count]; - for (var i = 0; i < srcDashes.Count; ++i) + var count = srcDashes.Count % 2 == 0 ? srcDashes.Count : srcDashes.Count * 2; + + var dashesArray = new float[count]; + + for (var i = 0; i < count; ++i) { - dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth; + dashesArray[i] = (float) srcDashes[i % srcDashes.Count] * paint.StrokeWidth; } var offset = (float)(pen.DashStyle.Offset * pen.Thickness); diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index a7cca5b0f3..f828b156da 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1409,7 +1409,7 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool TranslateMessage(ref MSG lpMsg); - [DllImport("user32.dll")] + [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern bool UnregisterClass(string lpClassName, IntPtr hInstance); [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SetWindowTextW")] diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs index 9b4eefd170..9a829aff92 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.ExceptionServices; using Avalonia.Logging; using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; @@ -88,8 +87,6 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics return null; } - return new AngleWin32PlatformGraphics(egl, AngleWin32EglDisplay.CreateSharedD3D11Display(egl)); - foreach (var api in (options?.AllowedPlatformApis ?? new [] { AngleOptions.PlatformApi.DirectX11 diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index fe7d881f11..8c362b0c29 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Remote; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; +using Avalonia.Threading; using Avalonia.Win32.Automation; using Avalonia.Win32.Input; using Avalonia.Win32.Interop.Automation; @@ -69,7 +70,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_CLOSE: { - bool? preventClosing = Closing?.Invoke(); + bool? preventClosing = Closing?.Invoke(WindowCloseReason.WindowClosing); if (preventClosing == true) { return IntPtr.Zero; @@ -106,6 +107,9 @@ namespace Avalonia.Win32 _touchDevice?.Dispose(); //Free other resources Dispose(); + + // Schedule cleanup of anything that requires window to be destroyed + Dispatcher.UIThread.Post(AfterCloseCleanup); return IntPtr.Zero; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9e11959101..b2ce68ee83 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -191,7 +191,7 @@ namespace Avalonia.Win32 public Action Activated { get; set; } - public Func Closing { get; set; } + public Func Closing { get; set; } public Action Closed { get; set; } @@ -643,12 +643,6 @@ namespace Avalonia.Win32 _hwnd = IntPtr.Zero; } - if (_className != null) - { - UnregisterClass(_className, GetModuleHandle(null)); - _className = null; - } - _framebuffer.Dispose(); } @@ -1144,6 +1138,15 @@ namespace Avalonia.Win32 } } + private void AfterCloseCleanup() + { + if (_className != null) + { + UnregisterClass(_className, GetModuleHandle(null)); + _className = null; + } + } + private void MaximizeWithoutCoveringTaskbar() { IntPtr monitor = MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST); diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index 602e5e4496..0162f53f5e 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -17,6 +17,9 @@ + + + diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index c42cb0241b..547b2cb176 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -12,6 +12,7 @@ using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; using Moq; +using Nito.AsyncEx; using Xunit; #nullable enable @@ -920,6 +921,120 @@ namespace Avalonia.Base.UnitTests } } + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Typed_Bind_Executes_On_UIThread(BindingPriority priority) + { + AsyncContext.Run(async () => + { + var target = new Class1(); + var source = new Subject(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + var raised = 0; + + var threadingInterfaceMock = new Mock(); + threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); + + var services = new TestServices( + threadingInterface: threadingInterfaceMock.Object); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + ++raised; + }; + + using (UnitTestApplication.Start(services)) + { + target.Bind(Class1.FooProperty, source, priority); + + await Task.Run(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(); + + Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); + Assert.Equal(1, raised); + } + }); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Untyped_Bind_Executes_On_UIThread(BindingPriority priority) + { + AsyncContext.Run(async () => + { + var target = new Class1(); + var source = new Subject(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + var raised = 0; + + var threadingInterfaceMock = new Mock(); + threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); + + var services = new TestServices( + threadingInterface: threadingInterfaceMock.Object); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + ++raised; + }; + + using (UnitTestApplication.Start(services)) + { + target.Bind(Class1.FooProperty, source, priority); + + await Task.Run(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(); + + Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); + Assert.Equal(1, raised); + } + }); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void BindingValue_Bind_Executes_On_UIThread(BindingPriority priority) + { + AsyncContext.Run(async () => + { + var target = new Class1(); + var source = new Subject>(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + var raised = 0; + + var threadingInterfaceMock = new Mock(); + threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); + + var services = new TestServices( + threadingInterface: threadingInterfaceMock.Object); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + ++raised; + }; + + using (UnitTestApplication.Start(services)) + { + target.Bind(Class1.FooProperty, source, priority); + + await Task.Run(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(); + + Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); + Assert.Equal(1, raised); + } + }); + } + [Fact] public async Task Bind_With_Scheduler_Executes_On_Scheduler() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 20172eea88..973090ee92 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -7,8 +7,10 @@ using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Logging; using Avalonia.Platform; +using Avalonia.Threading; using Avalonia.UnitTests; using Moq; +using Nito.AsyncEx; using Xunit; namespace Avalonia.Base.UnitTests @@ -519,25 +521,39 @@ namespace Avalonia.Base.UnitTests } [Fact] - public async Task Bind_Executes_On_UIThread() + public void Bind_Executes_On_UIThread() { - var target = new Class1(); - var source = new Subject(); - var currentThreadId = Thread.CurrentThread.ManagedThreadId; + AsyncContext.Run(async () => + { + var target = new Class1(); + var source = new Subject(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + var raised = 0; - var threadingInterfaceMock = new Mock(); - threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) - .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); + var threadingInterfaceMock = new Mock(); + threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); - var services = new TestServices( - threadingInterface: threadingInterfaceMock.Object); + var services = new TestServices( + threadingInterface: threadingInterfaceMock.Object); - using (UnitTestApplication.Start(services)) - { - target.Bind(Class1.FooProperty, source); + target.PropertyChanged += (s, e) => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + ++raised; + }; - await Task.Run(() => source.OnNext("foobar")); - } + using (UnitTestApplication.Start(services)) + { + target.Bind(Class1.FooProperty, source); + + await Task.Run(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(); + + Assert.Equal("foobar", target.Foo); + Assert.Equal(1, raised); + } + }); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 435d0d92ce..9dc0cb95bd 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -155,12 +155,16 @@ namespace Avalonia.Controls.UnitTests window.Closing += (sender, e) => { + Assert.Equal(WindowCloseReason.WindowClosing, e.CloseReason); + Assert.Equal(programaticClose, e.IsProgrammatic); count++; windowClosing = count; }; child.Closing += (sender, e) => { + Assert.Equal(WindowCloseReason.OwnerWindowClosing, e.CloseReason); + Assert.Equal(programaticClose, e.IsProgrammatic); count++; childClosing = count; }; @@ -186,7 +190,7 @@ namespace Avalonia.Controls.UnitTests } else { - var cancel = window.PlatformImpl.Closing(); + var cancel = window.PlatformImpl.Closing(WindowCloseReason.WindowClosing); Assert.Equal(false, cancel); } @@ -248,7 +252,7 @@ namespace Avalonia.Controls.UnitTests } else { - var cancel = window.PlatformImpl.Closing(); + var cancel = window.PlatformImpl.Closing(WindowCloseReason.WindowClosing); Assert.Equal(true, cancel); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs index d51d6122cd..59a17a8cd1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs @@ -166,6 +166,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data [Fact] public void Binding_Method_To_Command_Collected() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + WeakReference MakeRef() { var weakVm = new WeakReference(null); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs index 520abee59a..92807b2cb9 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -1,6 +1,8 @@ using System; +using System.Runtime.CompilerServices; using System.Xml; using Avalonia.Controls; +using Avalonia.Data; using Avalonia.Media; using Avalonia.Styling; using Xunit; @@ -9,6 +11,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml; public class MergeResourceIncludeTests { + static MergeResourceIncludeTests() + { + RuntimeHelpers.RunClassConstructor(typeof(RelativeSource).TypeHandle); + } + [Fact] public void MergeResourceInclude_Works_With_Single_Resource() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs index d76e51f419..5d6d4a78e4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Controls; +using Avalonia.Data; using Avalonia.Markup.Xaml.Styling; using Avalonia.Markup.Xaml.XamlIl.Runtime; using Avalonia.Media; @@ -15,6 +17,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml; public class StyleIncludeTests { + static StyleIncludeTests() + { + RuntimeHelpers.RunClassConstructor(typeof(RelativeSource).TypeHandle); + AssetLoader.RegisterResUriParsers(); + } + [Fact] public void StyleInclude_Is_Built() {