From 3304d646c007d3cbe72784bde5227ed76133d377 Mon Sep 17 00:00:00 2001 From: Fusion86 Date: Sun, 15 Nov 2020 18:49:23 +0100 Subject: [PATCH 01/26] Fix crash when KeyBindings change while they are being handled --- src/Avalonia.Input/KeyboardDevice.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 6f4cb7a35c..9a42e3d3a5 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -211,12 +211,18 @@ namespace Avalonia.Input { var bindings = (currentHandler as IInputElement)?.KeyBindings; if (bindings != null) - foreach (var binding in bindings) + { + // Create a copy of the KeyBindings list. + // If we don't do this the foreach loop will throw an InvalidOperationException when the KeyBindings list is changed. + // This can happen when a new view is loaded which adds its own KeyBindings to the handler. + var cpy = bindings.ToArray(); + foreach (var binding in cpy) { if (ev.Handled) break; binding.TryHandle(ev); } + } currentHandler = currentHandler.VisualParent; } From 38ce4a2a2891ef409c90f73790de05c0bc1ef314 Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Tue, 8 Dec 2020 18:41:40 +0300 Subject: [PATCH 02/26] Added ability to use non well known property types in style selector --- .../Activators/PropertyEqualsActivator.cs | 2 +- .../Styling/PropertyEqualsSelector.cs | 32 +++++++++++++++++-- .../SelectorTests_PropertyEquals.cs | 20 ++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs index 9e30e4fa14..8d446ebc9c 100644 --- a/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs +++ b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs @@ -33,6 +33,6 @@ namespace Avalonia.Styling.Activators void IObserver.OnCompleted() { } void IObserver.OnError(Exception error) { } - void IObserver.OnNext(object value) => PublishNext(Equals(value, _value)); + void IObserver.OnNext(object value) => PublishNext(PropertyEqualsSelector.Compare(_property.PropertyType, value, _value)); } } diff --git a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs index cdd985ac80..5d9c3fe56b 100644 --- a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs @@ -1,4 +1,6 @@ using System; +using System.ComponentModel; +using System.Globalization; using System.Text; using Avalonia.Styling.Activators; @@ -75,11 +77,37 @@ namespace Avalonia.Styling } else { - var result = (control.GetValue(_property) ?? string.Empty).Equals(_value); - return result ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; + return Compare(_property.PropertyType, control.GetValue(_property), _value) + ? SelectorMatch.AlwaysThisInstance + : SelectorMatch.NeverThisInstance; } + } protected override Selector? MovePrevious() => _previous; + + internal static bool Compare(Type propertyType, object propertyValue, object? value) + { + if (propertyType == typeof(object) && + propertyValue?.GetType() is Type inferredType) + { + propertyType = inferredType; + } + + var valueType = value?.GetType(); + + if (valueType is null || propertyType.IsAssignableFrom(valueType)) + { + return Equals(propertyValue, value); + } + + var converter = TypeDescriptor.GetConverter(propertyType); + if (converter?.CanConvertFrom(valueType) == true) + { + return Equals(propertyValue, converter.ConvertFrom(null, CultureInfo.InvariantCulture, value)); + } + + return false; + } } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs index e149410152..581a655c8e 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs @@ -22,6 +22,20 @@ namespace Avalonia.Styling.UnitTests Assert.False(await activator.Take(1)); } + [Fact] + public async Task PropertyEquals_Matches_When_Property_Has_Matching_Value_And_Different_Type() + { + var control = new TextBlock(); + var target = default(Selector).PropertyEquals(TextBlock.TagProperty, "Bar"); + var activator = target.Match(control).Activator.ToObservable(); + + Assert.False(await activator.Take(1)); + control.Tag = FooBar.Bar; + Assert.True(await activator.Take(1)); + control.Tag = null; + Assert.False(await activator.Take(1)); + } + [Fact] public void OfType_PropertyEquals_Doesnt_Match_Control_Of_Wrong_Type() { @@ -40,5 +54,11 @@ namespace Avalonia.Styling.UnitTests Assert.Equal("TextBlock[Text=foo]", target.ToString()); } + + private enum FooBar + { + Foo, + Bar + } } } From f75f6736260b27feb997d28b44a4e3605f5204e8 Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Fri, 11 Dec 2020 16:50:12 +0300 Subject: [PATCH 03/26] Added more test cases --- .../SelectorTests_PropertyEquals.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs index 581a655c8e..7689a458ae 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs @@ -22,15 +22,18 @@ namespace Avalonia.Styling.UnitTests Assert.False(await activator.Take(1)); } - [Fact] - public async Task PropertyEquals_Matches_When_Property_Has_Matching_Value_And_Different_Type() + [Theory] + [InlineData("Bar", FooBar.Bar)] + [InlineData("352", 352)] + [InlineData("0.1", 0.1)] + public async Task PropertyEquals_Matches_When_Property_Has_Matching_Value_And_Different_Type(string literal, object value) { var control = new TextBlock(); - var target = default(Selector).PropertyEquals(TextBlock.TagProperty, "Bar"); + var target = default(Selector).PropertyEquals(TextBlock.TagProperty, literal); var activator = target.Match(control).Activator.ToObservable(); Assert.False(await activator.Take(1)); - control.Tag = FooBar.Bar; + control.Tag = value; Assert.True(await activator.Take(1)); control.Tag = null; Assert.False(await activator.Take(1)); From 14ea7b44ed6c90ffbe3f5d58741e7aa545042e3c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Jun 2021 18:46:13 +0200 Subject: [PATCH 04/26] Fix caret positioning with empty newlines. `FormattedTextImpl` on Skia adds a special empty line when there is a trailing newline in the string. However this line is reported as having a width due to `SKPaint.MeasureText` returning a non-zero value for `\r` and `\n`. We can't simply always use 0 width when we encounter a control char when building the rects as that breaks hit testing. Instead, mark the empty trailing line with a flag and and treat it differently when building the rects and hit-testing a range. Yes, this is a big hack but as far as I understand the whole of `FormattedTextImpl` on Skia is a bit of a hack, but until @Gillibald's new `TextPresenter` is ready, this _might_ be a good enough fix? --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 5e630e54a6..5f4980e461 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Avalonia.Media; using Avalonia.Platform; @@ -175,7 +176,8 @@ namespace Avalonia.Skia foreach (var line in _skiaLines.Where(l => (l.Start + l.Length) > index && - lastIndex >= l.Start)) + lastIndex >= l.Start && + !l.IsEmptyTrailingLine)) { int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0); @@ -466,7 +468,8 @@ namespace Avalonia.Skia for (int i = line.Start; i < line.Start + line.TextLength; i++) { - float w = _paint.MeasureText(Text[i].ToString()); + var c = Text[i]; + var w = line.IsEmptyTrailingLine ? 0 :_paint.MeasureText(Text[i].ToString()); _rects.Add(new Rect( prevRight, @@ -611,6 +614,7 @@ namespace Avalonia.Skia lastLine.Width = lastLineWidth; lastLine.Height = _lineHeight; lastLine.Top = curY; + lastLine.IsEmptyTrailingLine = true; _skiaLines.Add(lastLine); @@ -713,6 +717,7 @@ namespace Avalonia.Skia public int TextLength; public float Top; public float Width; + public bool IsEmptyTrailingLine; }; private struct FBrushRange From a2dc40a14cafbd347d4784e147c9fb57c3f658bc Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 12 Jun 2021 12:03:53 +0200 Subject: [PATCH 05/26] Allow for using ScrollViewer.AllowAutoHide as an attached property. --- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 24 +++++++++++++ src/Avalonia.Controls/ScrollViewer.cs | 36 +++++++++++++++---- src/Avalonia.Themes.Default/ListBox.xaml | 3 +- src/Avalonia.Themes.Default/TextBox.xaml | 3 +- src/Avalonia.Themes.Default/TreeView.xaml | 3 +- .../Controls/ListBox.xaml | 3 +- .../Controls/TextBox.xaml | 3 +- .../Controls/TreeView.xaml | 3 +- 8 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 173c5c1a94..ae61f8f642 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -585,6 +585,30 @@ namespace Avalonia }); } + /// + /// Subscribes to a property changed notifications for changes that originate from a + /// . + /// + /// The type of the property change sender. + /// /// The type of the property.. + /// The property changed observable. + /// + /// The method to call. The parameters are the sender and the event args. + /// + /// A disposable that can be used to terminate the subscription. + public static IDisposable AddClassHandler( + this IObservable> observable, + Action> action) where TTarget : AvaloniaObject + { + return observable.Subscribe(e => + { + if (e.Sender is TTarget target) + { + action(target, e); + } + }); + } + /// /// Subscribes to a property changed notifications for changes that originate from a /// . diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 90c4e05943..559edeb204 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -176,8 +176,10 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty AllowAutoHideProperty = - ScrollBar.AllowAutoHideProperty.AddOwner(); + public static readonly AttachedProperty AllowAutoHideProperty = + AvaloniaProperty.RegisterAttached( + nameof(AllowAutoHide), + true); /// /// Defines the event. @@ -207,8 +209,8 @@ namespace Avalonia.Controls /// static ScrollViewer() { - HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); - VerticalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); + HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); + VerticalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); } /// @@ -526,6 +528,26 @@ namespace Avalonia.Controls return control.GetValue(VerticalScrollBarVisibilityProperty); } + /// + /// Gets the value of the AllowAutoHideProperty attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetAllowAutoHide(Control control, bool value) + { + control.SetValue(AllowAutoHideProperty, value); + } + + /// + /// Gets the value of the AllowAutoHideProperty attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static bool GetAllowAutoHide(Control control) + { + return control.GetValue(AllowAutoHideProperty); + } + /// /// Gets the value of the VerticalScrollBarVisibility attached property. /// @@ -604,10 +626,10 @@ namespace Avalonia.Controls CalculatedPropertiesChanged(); } - private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs e) + private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs e) { - var wasEnabled = !ScrollBarVisibility.Disabled.Equals(e.OldValue); - var isEnabled = !ScrollBarVisibility.Disabled.Equals(e.NewValue); + var wasEnabled = e.OldValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; + var isEnabled = e.NewValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; if (wasEnabled != isEnabled) { diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml index e91d8a6772..1ad996a1a0 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/ListBox.xaml @@ -13,7 +13,8 @@ + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> Date: Sat, 12 Jun 2021 22:42:59 +0200 Subject: [PATCH 06/26] Add PolyLineSegment path segment --- src/Avalonia.Visuals/Media/PolyLineSegment.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/Avalonia.Visuals/Media/PolyLineSegment.cs diff --git a/src/Avalonia.Visuals/Media/PolyLineSegment.cs b/src/Avalonia.Visuals/Media/PolyLineSegment.cs new file mode 100644 index 0000000000..55bfb33041 --- /dev/null +++ b/src/Avalonia.Visuals/Media/PolyLineSegment.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Avalonia.Collections; + +namespace Avalonia.Media +{ + /// + /// Represents a set of line segments defined by a points collection with each Point specifying the end point of a line segment. + /// + public sealed class PolyLineSegment : PathSegment + { + /// + /// Defines the property. + /// + public static readonly StyledProperty PointsProperty + = AvaloniaProperty.Register(nameof(Points)); + + /// + /// Gets or sets the points. + /// + /// + /// The points. + /// + public AvaloniaList Points + { + get => GetValue(PointsProperty); + set => SetValue(PointsProperty, value); + } + + /// + /// Initializes a new instance of the class. + /// + public PolyLineSegment() + { + Points = new Points(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The points. + public PolyLineSegment(IEnumerable points) : this() + { + Points.AddRange(points); + } + + protected internal override void ApplyTo(StreamGeometryContext ctx) + { + var points = Points; + if (points.Count > 0) + { + for (int i = 0; i < points.Count; i++) + { + ctx.LineTo(points[i]); + } + } + } + + public override string ToString() + => Points.Count >= 1 ? "L " + string.Join(" ", Points) : ""; + } +} From d6d87b1a32c91bbaa9c066b95d20350bee047ad1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 14 Jun 2021 12:31:45 +0200 Subject: [PATCH 07/26] Fix fetching neutral value in animations. Animators fetched the neutral value when the animation is instantiated but did not update the neutral value if it's updated after that. #6063 was caused by the fact that the neutral value does not take effect until the batch update due to styling has finished on the control, which is _after_ the animation has been instantiated. Listen for changes on the property and if the change is not an animated value change, update the neutral value. This isn't perfect because it won't react to changes to the neutral value while the animation is actually running and producing values because `PropertyChanged` events don't get fired for non-active values: for that we'd need to hook into `OnPropertyChangedCore`. But this at least fixes the simple case of the initial neutral value. --- src/Avalonia.Animation/AnimationInstance`1.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index 6f601a3e13..0e1882ce75 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -5,6 +5,7 @@ using Avalonia.Animation.Animators; using Avalonia.Animation.Utils; using Avalonia.Data; using Avalonia.Reactive; +using JetBrains.Annotations; namespace Avalonia.Animation { @@ -45,8 +46,9 @@ namespace Avalonia.Animation _onCompleteAction = OnComplete; _interpolator = Interpolator; _baseClock = baseClock; - _neutralValue = (T)_targetControl.GetValue(_animator.Property); + control.PropertyChanged += ControlPropertyChanged; + UpdateNeutralValue(); FetchProperties(); } @@ -216,5 +218,22 @@ namespace Avalonia.Animation } } } + + private void UpdateNeutralValue() + { + var property = _animator.Property; + var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue); + + _neutralValue = baseValue != AvaloniaProperty.UnsetValue ? + (T)baseValue : (T)_targetControl.GetValue(property); + } + + private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation) + { + UpdateNeutralValue(); + } + } } } From 6b94cc4ed99dd9b2ee86b2396da5d9a7a6e788ab Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 14 Jun 2021 12:56:22 +0200 Subject: [PATCH 08/26] Added failing test for #5054. --- .../KeyboardDeviceTests.cs | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs index df0a077c7f..5b39e23ba6 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs @@ -1,5 +1,9 @@ -using Avalonia.Input.Raw; +using System; +using System.Windows.Input; +using Avalonia.Controls; +using Avalonia.Input.Raw; using Avalonia.Interactivity; +using Avalonia.UnitTests; using Moq; using Xunit; @@ -86,5 +90,38 @@ namespace Avalonia.Input.UnitTests focused.Verify(x => x.RaiseEvent(It.IsAny())); } + + [Fact] + public void Can_Change_KeyBindings_In_Keybinding_Event_Handler() + { + var target = new KeyboardDevice(); + var button = new Button(); + var root = new TestRoot(button); + + button.KeyBindings.Add(new KeyBinding + { + Gesture = new KeyGesture(Key.O, KeyModifiers.Control), + Command = new DelegateCommand(() => button.KeyBindings.Clear()), + }); + + target.SetFocusedElement(button, NavigationMethod.Pointer, 0); + target.ProcessRawEvent( + new RawKeyEventArgs( + target, + 0, + root, + RawKeyEventType.KeyDown, + Key.O, + RawInputModifiers.Control)); + } + + private class DelegateCommand : ICommand + { + private readonly Action _action; + public DelegateCommand(Action action) => _action = action; + public event EventHandler CanExecuteChanged; + public bool CanExecute(object parameter) => true; + public void Execute(object parameter) => _action(); + } } } From 4285c3d0d1981993eda9fd9fe977e87fdcf5acf0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 14 Jun 2021 13:04:56 +0200 Subject: [PATCH 09/26] Don't create a copy of the array unless necessary. --- src/Avalonia.Input/KeyboardDevice.cs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index bf2f689785..a159b19026 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -218,15 +218,28 @@ namespace Avalonia.Input var bindings = (currentHandler as IInputElement)?.KeyBindings; if (bindings != null) { - // Create a copy of the KeyBindings list. + KeyBinding[]? bindingsCopy = null; + + // Create a copy of the KeyBindings list if there's a binding which matches the event. // If we don't do this the foreach loop will throw an InvalidOperationException when the KeyBindings list is changed. // This can happen when a new view is loaded which adds its own KeyBindings to the handler. - var cpy = bindings.ToArray(); - foreach (var binding in cpy) + foreach (var binding in bindings) { - if (ev.Handled) + if (binding.Gesture?.Matches(ev) == true) + { + bindingsCopy = bindings.ToArray(); break; - binding.TryHandle(ev); + } + } + + if (bindingsCopy is object) + { + foreach (var binding in bindingsCopy) + { + if (ev.Handled) + break; + binding.TryHandle(ev); + } } } currentHandler = currentHandler.VisualParent; From a11270b07e5edc062bbe37b4b356598bf574c0c7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 14 Jun 2021 13:07:28 +0200 Subject: [PATCH 10/26] Add a sanity check to the test. --- tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs index 5b39e23ba6..7730cee78c 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs @@ -97,11 +97,16 @@ namespace Avalonia.Input.UnitTests var target = new KeyboardDevice(); var button = new Button(); var root = new TestRoot(button); + var raised = 0; button.KeyBindings.Add(new KeyBinding { Gesture = new KeyGesture(Key.O, KeyModifiers.Control), - Command = new DelegateCommand(() => button.KeyBindings.Clear()), + Command = new DelegateCommand(() => + { + button.KeyBindings.Clear(); + ++raised; + }), }); target.SetFocusedElement(button, NavigationMethod.Pointer, 0); @@ -113,6 +118,8 @@ namespace Avalonia.Input.UnitTests RawKeyEventType.KeyDown, Key.O, RawInputModifiers.Control)); + + Assert.Equal(1, raised); } private class DelegateCommand : ICommand From 82754cb1633eb2d93fd29fd6ef10962267e2b9c9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 14 Jun 2021 15:29:33 +0200 Subject: [PATCH 11/26] Fixed gradient brush target rectangles. When drawn via the `DrawingContext` gradient brushes were not taking the origin of the target rect into account, only the size. Fixes #5947. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 47 +++++++++--------- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 4 +- .../Media/AvaloniaTextRenderer.cs | 4 +- .../Media/DrawingContextImpl.cs | 32 ++++++------ .../Media/LinearGradientBrushImpl.cs | 7 +-- .../Media/RadialGradientBrushImpl.cs | 11 ++-- .../Media/ConicGradientBrushTests.cs | 40 +++++++++++++++ .../Media/LinearGradientBrushTests.cs | 36 ++++++++++++++ .../Media/RadialGradientBrushTests.cs | 34 +++++++++++++ ...cGradientBrush_DrawingContext.expected.png | Bin 0 -> 631 bytes ...rGradientBrush_DrawingContext.expected.png | Bin 0 -> 2997 bytes ...lGradientBrush_DrawingContext.expected.png | Bin 0 -> 9127 bytes ...cGradientBrush_DrawingContext.expected.png | Bin 0 -> 10037 bytes ...rGradientBrush_DrawingContext.expected.png | Bin 0 -> 1908 bytes ...lGradientBrush_DrawingContext.expected.png | Bin 0 -> 5904 bytes 15 files changed, 163 insertions(+), 52 deletions(-) create mode 100644 tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png create mode 100644 tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png create mode 100644 tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png create mode 100644 tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2c5c2acd52..d8bd0607d1 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -164,7 +164,7 @@ namespace Avalonia.Skia /// public void DrawLine(IPen pen, Point p1, Point p2) { - using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) + using (var paint = CreatePaint(_strokePaint, pen, new Rect(p1, p2).Normalize())) { if (paint.Paint is object) { @@ -177,10 +177,10 @@ namespace Avalonia.Skia public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { var impl = (GeometryImpl) geometry; - var size = geometry.Bounds.Size; + var rect = geometry.Bounds; - using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper)) - using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper)) + using (var fill = brush != null ? CreatePaint(_fillPaint, brush, rect) : default(PaintWrapper)) + using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, rect) : default(PaintWrapper)) { if (fill.Paint != null) { @@ -354,7 +354,7 @@ namespace Avalonia.Skia if (brush != null) { - using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size)) + using (var paint = CreatePaint(_fillPaint, brush, rect.Rect)) { if (isRounded) { @@ -397,7 +397,7 @@ namespace Avalonia.Skia if (pen?.Brush != null) { - using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size)) + using (var paint = CreatePaint(_strokePaint, pen, rect.Rect)) { if (paint.Paint is object) { @@ -417,7 +417,7 @@ namespace Avalonia.Skia /// public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { - using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size)) + using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds)) { var textImpl = (FormattedTextImpl) text; textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); @@ -427,7 +427,7 @@ namespace Avalonia.Skia /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, new Rect(glyphRun.Size))) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; @@ -537,7 +537,7 @@ namespace Avalonia.Skia var paint = new SKPaint(); Canvas.SaveLayer(paint); - _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true)); + _maskStack.Push(CreatePaint(paint, mask, bounds, true)); } /// @@ -593,18 +593,19 @@ namespace Avalonia.Skia /// Paint wrapper. /// Target size. /// Gradient brush. - private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush) + private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect targetRect, IGradientBrush gradientBrush) { var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode(); var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray(); + var position = targetRect.Position.ToSKPoint(); switch (gradientBrush) { case ILinearGradientBrush linearGradient: { - var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint(); - var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint(); + var start = position + linearGradient.StartPoint.ToPixels(targetRect.Size).ToSKPoint(); + var end = position + linearGradient.EndPoint.ToPixels(targetRect.Size).ToSKPoint(); // would be nice to cache these shaders possibly? using (var shader = @@ -617,10 +618,10 @@ namespace Avalonia.Skia } case IRadialGradientBrush radialGradient: { - var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); - var radius = (float)(radialGradient.Radius * targetSize.Width); + var center = position + radialGradient.Center.ToPixels(targetRect.Size).ToSKPoint(); + var radius = (float)(radialGradient.Radius * targetRect.Width); - var origin = radialGradient.GradientOrigin.ToPixels(targetSize).ToSKPoint(); + var origin = position + radialGradient.GradientOrigin.ToPixels(targetRect.Size).ToSKPoint(); if (origin.Equals(center)) { @@ -665,7 +666,7 @@ namespace Avalonia.Skia } case IConicGradientBrush conicGradient: { - var center = conicGradient.Center.ToPixels(targetSize).ToSKPoint(); + var center = position + conicGradient.Center.ToPixels(targetRect.Size).ToSKPoint(); // Skia's default is that angle 0 is from the right hand side of the center point // but we are matching CSS where the vertical point above the center is 0. @@ -867,10 +868,10 @@ namespace Avalonia.Skia /// /// The paint to wrap. /// Source brush. - /// Target size. + /// Target rect. /// Optional dispose of the supplied paint. /// Paint wrapper for given brush. - internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false) + internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Rect targetRect, bool disposePaint = false) { var paintWrapper = new PaintWrapper(paint, disposePaint); @@ -889,7 +890,7 @@ namespace Avalonia.Skia if (brush is IGradientBrush gradient) { - ConfigureGradientBrush(ref paintWrapper, targetSize, gradient); + ConfigureGradientBrush(ref paintWrapper, targetRect, gradient); return paintWrapper; } @@ -909,7 +910,7 @@ namespace Avalonia.Skia if (tileBrush != null && tileBrushImage != null) { - ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage); + ConfigureTileBrush(ref paintWrapper, targetRect.Size, tileBrush, tileBrushImage); } else { @@ -924,10 +925,10 @@ namespace Avalonia.Skia /// /// The paint to wrap. /// Source pen. - /// Target size. + /// Target rect. /// Optional dispose of the supplied paint. /// - private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false) + private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Rect targetRect, bool disposePaint = false) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. @@ -936,7 +937,7 @@ namespace Avalonia.Skia return default; } - var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint); + var rv = CreatePaint(paint, pen.Brush, targetRect, disposePaint); paint.IsStroke = true; paint.StrokeWidth = (float) pen.Thickness; diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 5f4980e461..3eca42faa9 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -278,9 +278,9 @@ namespace Avalonia.Skia if (fb != null) { - //TODO: figure out how to get the brush size + //TODO: figure out how to get the brush rect currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb, - new Size()); + default); } else { diff --git a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs index 22c998df93..e4b2405290 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs @@ -34,10 +34,10 @@ namespace Avalonia.Direct2D1.Media { var wrapper = clientDrawingEffect as BrushWrapper; - // TODO: Work out how to get the size below rather than passing new Size(). + // TODO: Work out how to get the rect below rather than passing default. var brush = (wrapper == null) ? _foreground : - _context.CreateBrush(wrapper.Brush, new Size()).PlatformBrush; + _context.CreateBrush(wrapper.Brush, default).PlatformBrush; _renderTarget.DrawGlyphRun( new RawVector2 { X = baselineOriginX, Y = baselineOriginY }, diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index af35934785..9336c9a7bb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -192,7 +192,7 @@ namespace Avalonia.Direct2D1.Media { using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value)) - using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size)) + using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect)) using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(Direct2D1Platform.Direct2D1Factory, destRect.ToDirect2D())) { if (d2dOpacityMask.PlatformBrush != null) @@ -217,9 +217,7 @@ namespace Avalonia.Direct2D1.Media { if (pen != null) { - var size = new Rect(p1, p2).Size; - - using (var d2dBrush = CreateBrush(pen.Brush, size)) + using (var d2dBrush = CreateBrush(pen.Brush, new Rect(p1, p2).Normalize())) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) { if (d2dBrush.PlatformBrush != null) @@ -245,7 +243,7 @@ namespace Avalonia.Direct2D1.Media { if (brush != null) { - using (var d2dBrush = CreateBrush(brush, geometry.Bounds.Size)) + using (var d2dBrush = CreateBrush(brush, geometry.Bounds)) { if (d2dBrush.PlatformBrush != null) { @@ -257,7 +255,7 @@ namespace Avalonia.Direct2D1.Media if (pen != null) { - using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen).Size)) + using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen))) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) { if (d2dBrush.PlatformBrush != null) @@ -282,7 +280,7 @@ namespace Avalonia.Direct2D1.Media if (brush != null) { - using (var b = CreateBrush(brush, rect.Size)) + using (var b = CreateBrush(brush, rect)) { if (b.PlatformBrush != null) { @@ -311,7 +309,7 @@ namespace Avalonia.Direct2D1.Media if (pen?.Brush != null) { - using (var wrapper = CreateBrush(pen.Brush, rect.Size)) + using (var wrapper = CreateBrush(pen.Brush, rect)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) { if (wrapper.PlatformBrush != null) @@ -349,7 +347,7 @@ namespace Avalonia.Direct2D1.Media { var impl = (FormattedTextImpl)text; - using (var brush = CreateBrush(foreground, impl.Bounds.Size)) + using (var brush = CreateBrush(foreground, impl.Bounds)) using (var renderer = new AvaloniaTextRenderer(this, _deviceContext, brush.PlatformBrush)) { if (brush.PlatformBrush != null) @@ -367,7 +365,7 @@ namespace Avalonia.Direct2D1.Media /// The glyph run. public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Size)) + using (var brush = CreateBrush(foreground, new Rect(glyphRun.Size))) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; @@ -458,9 +456,9 @@ namespace Avalonia.Direct2D1.Media /// Creates a Direct2D brush wrapper for a Avalonia brush. /// /// The avalonia brush. - /// The size of the brush's target area. + /// The brush's target area. /// The Direct2D brush wrapper. - public BrushImpl CreateBrush(IBrush brush, Size destinationSize) + public BrushImpl CreateBrush(IBrush brush, Rect destinationRect) { var solidColorBrush = brush as ISolidColorBrush; var linearGradientBrush = brush as ILinearGradientBrush; @@ -475,11 +473,11 @@ namespace Avalonia.Direct2D1.Media } else if (linearGradientBrush != null) { - return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationSize); + return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationRect); } else if (radialGradientBrush != null) { - return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationSize); + return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationRect); } else if (conicGradientBrush != null) { @@ -492,7 +490,7 @@ namespace Avalonia.Direct2D1.Media imageBrush, _deviceContext, (BitmapImpl)imageBrush.Source.PlatformImpl.Item, - destinationSize); + destinationRect.Size); } else if (visualBrush != null) { @@ -523,7 +521,7 @@ namespace Avalonia.Direct2D1.Media visualBrush, _deviceContext, new D2DBitmapImpl(intermediate.Bitmap), - destinationSize); + destinationRect.Size); } } } @@ -574,7 +572,7 @@ namespace Avalonia.Direct2D1.Media ContentBounds = PrimitiveExtensions.RectangleInfinite, MaskTransform = PrimitiveExtensions.Matrix3x2Identity, Opacity = 1, - OpacityBrush = CreateBrush(mask, bounds.Size).PlatformBrush + OpacityBrush = CreateBrush(mask, bounds).PlatformBrush }; var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext); _deviceContext.PushLayer(ref parameters, layer); diff --git a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs index 0e63d4cc03..69b45455ac 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs @@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media public LinearGradientBrushImpl( ILinearGradientBrush brush, SharpDX.Direct2D1.RenderTarget target, - Size destinationSize) + Rect destinationRect) { if (brush.GradientStops.Count == 0) { @@ -21,8 +21,9 @@ namespace Avalonia.Direct2D1.Media Position = (float)s.Offset }).ToArray(); - var startPoint = brush.StartPoint.ToPixels(destinationSize); - var endPoint = brush.EndPoint.ToPixels(destinationSize); + var position = destinationRect.Position; + var startPoint = position + brush.StartPoint.ToPixels(destinationRect.Size); + var endPoint = position + brush.EndPoint.ToPixels(destinationRect.Size); using (var stops = new SharpDX.Direct2D1.GradientStopCollection( target, diff --git a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs index 1fca6d4e33..7dcfd7e1e0 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs @@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media public RadialGradientBrushImpl( IRadialGradientBrush brush, SharpDX.Direct2D1.RenderTarget target, - Size destinationSize) + Rect destinationRect) { if (brush.GradientStops.Count == 0) { @@ -21,12 +21,13 @@ namespace Avalonia.Direct2D1.Media Position = (float)s.Offset }).ToArray(); - var centerPoint = brush.Center.ToPixels(destinationSize); - var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize) - centerPoint; + var position = destinationRect.Position; + var centerPoint = position + brush.Center.ToPixels(destinationRect.Size); + var gradientOrigin = position + brush.GradientOrigin.ToPixels(destinationRect.Size) - centerPoint; // Note: Direct2D supports RadiusX and RadiusY but Cairo backend supports only Radius property - var radiusX = brush.Radius * destinationSize.Width; - var radiusY = brush.Radius * destinationSize.Height; + var radiusX = brush.Radius * destinationRect.Width; + var radiusY = brush.Radius * destinationRect.Height; using (var stops = new SharpDX.Direct2D1.GradientStopCollection( target, diff --git a/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs index db69a0a028..3d2f09e2a8 100644 --- a/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Media; +using System; using System.Threading.Tasks; using Xunit; @@ -174,5 +175,44 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } + + [Fact] + public async Task ConicGradientBrush_DrawingContext() + { + var brush = new ConicGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Yellow, Offset = 0.1667 }, + new GradientStop { Color = Colors.Lime, Offset = 0.3333 }, + new GradientStop { Color = Colors.Aqua, Offset = 0.5000 }, + new GradientStop { Color = Colors.Blue, Offset = 0.6667 }, + new GradientStop { Color = Colors.Magenta, Offset = 0.8333 }, + new GradientStop { Color = Colors.Red, Offset = 1 }, + } + }; + + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new DrawnControl(c => + { + c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); + c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100)); + }), + }; + + await RenderToFile(target); + CompareImages(); + } + + private class DrawnControl : Control + { + private readonly Action _render; + public DrawnControl(Action render) => _render = render; + public override void Render(DrawingContext context) => _render(context); + } } } diff --git a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs index af7b4f130f..28701f2f97 100644 --- a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs @@ -72,5 +72,41 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } + + [Fact] + public async Task LinearGradientBrush_DrawingContext() + { + var brush = new LinearGradientBrush + { + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative), + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + } + }; + + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new DrawnControl(c => + { + c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); + c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100)); + }), + }; + + await RenderToFile(target); + CompareImages(); + } + + private class DrawnControl : Control + { + private readonly Action _render; + public DrawnControl(Action render) => _render = render; + public override void Render(DrawingContext context) => _render(context); + } } } diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index 2941b8dc34..95fae7f2fa 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -165,5 +165,39 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } + + [Fact] + public async Task RadialGradientBrush_DrawingContext() + { + var brush = new RadialGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + } + }; + + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new DrawnControl(c => + { + c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); + c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100)); + }), + }; + + await RenderToFile(target); + CompareImages(); + } + + private class DrawnControl : Control + { + private readonly Action _render; + public DrawnControl(Action render) => _render = render; + public override void Render(DrawingContext context) => _render(context); + } } } diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..3a2241b53f92cc878ba874d1af5fb67efd875848 GIT binary patch literal 631 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 HoGAW30rASt literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..85d11f2c2bb1cb02ee1f4b84770ef83fac040e93 GIT binary patch literal 9127 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 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..3906d5a181a2fdb7ee70e1ce1f01820608d1a4de GIT binary patch literal 10037 zcmb7~cTkf-*XUEI0Ya|^3?WG9sDL1#p+o|RQbQ;LQj{Wy0hK0%CRGSsX+oq(Kp=D! zlpY`=MLLKeK@>rnAS&t?zx&UfJNLVH-pnM=%rl$avuDqqbN06<(c+RZ4@3e2004MQ zutrwQ=fJ-&2OIPE8NQ~7`Cth&FtOoae!@9C5&!@=z{E)3Cb(d;(7ZvWZ!9jYbn3T+ z7+8wkkcGSJjpJ0^!|!rC(?{$ZadxQzDY?!)AlQAinVtpSCWkHZ*~!`2NA~v5zZ*Qb zV$%4+VLaQRC^YEOHCww{=-~=b46t!>)6;Q2v~2i$y>d(8_|$>U3jW3y;7s@8*%kae zy?H^?G=GcPV#+j(r}_`CV&l^j&%dd<_jS!`ZfG2%x+FI*;Fv9%9>nh+aYm9O8t)_# z=23pn-iGCa;(uKp`(T%-@|!-O#quXB#tIbAmK56qE?Z)s3gg;bA-EbzF9U)h}KvyK`3`C^hUY@5ns6%hy#V=-nE z+AOOe3N)GGDq09ouGDh)WFNDWGnT=c^dWtZ{F|^O0o$D=3pmxe9B4@q8S|J288q1O zkSH?lR1YJ8AT#|^0>gEiz>kgtf2ho8Nuk8DrrLlNk5g0g`zWWx;Lfy6SAdxXVm&>^ z2z=qdDGQPoJmRN+st)VGE}?`u2&j;LoMFu-~)R-KF=Z~u{D=?2K`5_8$14eLpI9g2H@oW zl7fJ3TC~;8(q2ajLN;Gh&P>V?_Y7#|=|RB@i2P``;Z1NKapo5*m9oMRItKzRUtaJQ zzZ)Zql2Q-vCbM5O(w2bRvK>P$BzLeGBMGJmKhY*_wJ}N#{}?5kEp~daqxnq3nd{WU zml)eFKsw2SMaL6m(vlVXUE=JUF6rjiZ@I9#FEXt`ReQ$e9bqC>K&vZ3pIt1hc`jqf zChcrVEJiWiRsvA-6NHfao>cOWxVwc|BmL50ACamd3nh`>TaE3MRi*)C?m-3NFaf$7 z>xgUx!Z}-k%r}Lx7BRB)om9eQOAC4Mg;kz#NVL6Ubu+2`T0{He8K-)cb1r71Th`B# zoXowVo^<6j*qQ57xXR&Kyk6@iw)F+3<0sWET0=*R)({R``&qqO8ax7Da&@m~n>HtO z)K5H-$KJlmfNLk;c0sEPm+TeaOH8UmS*9gqhjOFa#|g<415`pIzA^G5x-}ZB)#w~V z?i*j+J!iTjMpS35RCzAd8G1DG217P=`(3}L;1JH5jXg&oM`tl4B)?^wHws%+FCak! zGv$9p9VUcj;NP|yyCA-wh(-!#ZFNf|Gwyw?-ZNwF4BK|M+etonI;%Z|T$zB=a0JIs zHV#;UOJ6<&X4HJdUwy!XJB@CAKHw}S|BA3mqDjCdU;-H_(Tsj=gjxWK01=?t!BX~* z^3R~xxmo$3o;|JI_m0b&lsRm^%`-8 zRE>}LkbOl1PsA9rxz1NBm&1%s@9qX-jCOg=PBsJHWF0)(or`8E+)aVOct}6pfE@Z= z&wi=9QM)AyL6H3+sa5U7DbK^_FLji{+|^8`m24{=3gpvR=`XUbW3|-PB;dF2LlW4A z)O0@nNPNv{s(3|0sqv}C{wK+e@!C2(&ctpG0Y&>K&nN;DoItwl7r>=&v_NESK~Bhf zIgE+;P}XBF1vds4jYJ$EsM_%<@`}skMp~?Wl>94%F4$FB6)z)>w%oL|3z{g@&|6xU z20ps~4fn}r-cz2lE*1kaP$1mr6j(2`!o>EC6QGqMB4TT9wf0Kj)Sl1?Lk=$P#T>TG zk$8Vg;~@+bzO^ZAHMK=s)-!%vN?Gxc>$piIMQ-iRvm&#-BjcvN7;j;`7*|CP>F>w2< z$)|5Dcq4@bG8|t=u7B3MfL7PyNKte@y)hZl(Z2TeURF;`^zoLbicV+(Y5TYzm(aO^ z*HN=>50q8vrY_9e$V%tmj`_S6k<1GBc=mKfkmGW1Igr4{{ciW;+nlEQeyzqG9^o{5e(pqIRsk7HmtEW2Nq=xUNGs5IC~D%i*F}a%r^X2^3!H@}dqQlH{}`})FRdZ= zz2t6yky7@>eq}Aj)PJiBwx(j6Rj``DdLnui+Nu4f&Q|Vh`sxP-!h?wyBedx>;tg6n zN9tWbJH5H$A)u{b(XHju%vf81{b>fJ)eROUi;1dxgs+EoTr|qDhU9GNAxf|vRM`Q^n#^dIeaytbJ?~WL1Av!(@to{ zj$t%;82yTMbx2ZWM`bEGLxuEwO_?~2)%&fh*CL*OSuJYkPwPE3sWcH^t;-TNAevz` zaIF<{r33`()t4W%Ze&@^yB7go5?E_Jo0EI?;+-nv^$+I}vVt;jJ1jgLup~aeNaH7+ zS#eo}KMusZF37zaO^ss1J{#NL=mf=`7X@C3=`&??n6!vuC4X`*cCAt;8YM@Tpq-bo z&OZ545u%+pf#DoW+QhW}mG8ta&_nTau&IRBatVHvD~x^*{ABUdosG`un7ImcW=xz- zoZWK{q489kvdwLwk$_Z1WH>Q)`y^2maE^6bIv|Yo=on|}nTI#BS)Rl{ z4qq!K-r`o0u5gqUs%Dhl{!(Kq+p6uAfwAjh#`Kl>`*{@}-js5!;*nw41b zX&a=vqYVh(XKxSX41d7V&vOE$?BRg{KXv(g$YqUxJ=^Of-WzQtnq+V_LI!^F&p)XO{(pe_fkMjDK zMX;Cdyt)8d2}3HzsRF}U0WQnJM7zV&=k(LAV~a$Alp^e$!rpo2Q0HaeU1O}*wL}(7{%=q7 z@86SnIX0fr8dpU2V<8Ew7v#B5?Mx0;g!2f72%pXNc<&m$ascrzSg}T;&lyr&CF%I{ z06?hI2c0AE-|l=Fq}@YP8iA9e-fgbTX|6iT$_o*f!W0EWvd%7Hfz*iX4h80^VFl++ zv}<$#Thcs7_hOM^$7LSea{}x|G40U4Xj$a^(&Iz^+Ogx=u^)kZRX(ee@**gHylHkh z(CTK8-X*(um{49^eRUQq?HGKxgxqF3Lb@<`-YD7=hIN6Ky?=1M=sv7=N3domRFi%s z#;CzLhgC7E%*NAUg8v=#2e6KV59eGGwcMrOye{EwwY(z%)utt#fTvnhb26zlw7cx( zpdIbSB@O?8u-7KN!p#au5tQ@G4?yt0owW?w6>~LO`lRz(H8`Uo@W;gdty5csvs+(w z-W{x*$v;ugeJFl@MWW?XGCah~LIS=|p4h8dh_&5&GEF0N;RCeiDQ|4 zIi?7j1%gySKB2sxXrJLs5euqTcIp)FiFRjq9mPp6b14t_4tmP32X{$V#Q|z(j`{7?Ot+>VB14{drMatJ*X>_KBPv(#~Ox ztJV99owp*q-jiDX#$P$5Z*6B++T!=A8ofI+?(tCe=JM&rpmeqYd6L&&kHdtZqhuqUt?8xGnU`nDu`!`D)EC|)3wd;m(Vv_V{m;}MDLt=_t6UjRGZ+fa%Iir_ zqVcZ5tByR}JS2T*g@=wdc`+r=@~f1$UjmFA3vw?L*KX@@4#2X&cV!!KN^vKT&}U=tfDD3zCSaTBPs zP(ovie?rC+`HaeZ#{Wf?fqQS z0eoSjX|sdGOGlAq`iRR{445)<$S1J&OLf-4Ob5W^clJ2zl!bzzX}6us563Sh{(N+Z zvofh3gwJh7ruE>(7FPm}7+1q{4vwhh@SDa#r#zK`9#ibA&JK!PB$(LZYGiU3xXP(& zR=Lfnpdh`xqgLZ}3!C?6;_p^vAa!ZUG5L8ntC8Xw)b)-jxYs%j1$R`EZt$tB=^M}T zpv@i48w1l_zdek}l)7B*s}y z8>iUsmJ+_mJJjK=xn)t_Iw>-8V5l*+bk&DAA<4zeH`s>f)&OH0*|A1#nKuq7WjAoX zyoaAzwrKZ+po>#CrmaMw?j62jdxD|&4c{_j?=_-V!0Vs6jr&D?^#FNGZqCO!kx#V~ za^t<^D3jZVlP6CpS$Om%pTE#Hxb#ndYCvrz`k&@d{G|(Ero+MQn}K5-@0~Hpef1Gn zX@26%SV3%L(e(~g)MK>CLvGr2UN7Q=1Q!~*aLInXL_Q(AIE?cJd%WgdLjyMlB_1f< z$D0^z_tE$9s(npcQt5%(N4dc}8tC;0;_&r^oH7{y8lq%7%b)?*b|jNu8WzGaJlG^; z)Sba~SzilQwR4X%3#fO%Lo$_lho=3c4g)`A-&Ou4WcvUGOHMFAyh>n8xC5-;x3`?v zV2|w-@Bhox?mM#JIV4;*--c(?NnJ}so1@s!?cT}gCo#|j&$j9W;wjXxS-u;|rewqT zw@bks==SduSu2YWvh2oOwj>MP9aw+nV8Y+h?2!5D(!9qWL$;#bSVjPzlF@+g6et#HiY<%@z9;8KRZ385 zBDpebkjJ$5Ni<#2<+of!(&(fzs*P{I>rU?A87+{t?_NEPJ<^BmAC&iK{{MN?k%XNNu^&gnR zj)AWIZn>-v;&CP^KsddwF*v_l%49^|!27EJ)kfQ^xKWf}em|!mmMywWN&w2s?N?n| ziS-++3}9q@_4>+hnIU`wuDj16`?(PYSkpI`VkeSPCYEl7d;uOZh~*C%ecv;PCdaZCX#b_^XcbJ2%7Qf6VnX zPWx9WY&omW&qZ8^CHv_y>_!`N3MWN`Uq8E_`2s2*YMyaO-P--@p&T(gVEeM&RHDx2 zbY;*hQt99EXn*@ZVQ0x~*xOW`R5JdQpVA2sP4%JBxjUv0>B*M|Dv!Y@F-H2q^AgF4 z-8)=4!jXw&w;DgaDIta*r@4OPXuo~yaFRki?29}wW@&&(?)WBTVM?1NuLNJ}s$?RX z8){qTLPf*%1U}!3_pix3CM!t6Ajm1YI^1*;k$1xp?R7u_T7?<)Uq&G~pO?X{v^3PeG24+^!E?bO6REd9pic(Mgf;5Yvgd51({CP| zMI_#30e#VvfH%dnASDPCJv&}bgRe8YI&H{zRQ$w&O{tMa`%1zy$Xo++x+Oq?D9j7; z7=rCpg$%+@Qo-<9ASt0^$i57Iu(S zi;}l{lmd_$!TgXY^IYJ`DGnZG;0{u%dEWsl^)w%%m%?~bv5YLEITfEkSF)|L4B1wZ zhod$#Y;#a9l>@YU zeo9ZgjDQzW3|7$=-l5p=$JMFi)x#kURoAE#o*MY>oiQu(MuDo0(nX<=;cFr1scjxt z!e8bTzpro*Zp#}58OJEQ*XBjrh?l~T`irde8clPoP0rqDOKN|js`}l*nPx4pGNF#G zcG)ekRhUH_3|Xa@!!_>b#4wbm?g7VaV&B9Z$SQP`jr0(>^^<^GrgR?CdbHyI1DdIZ z0>12=*AY!rNZh9vRe9|Q+aP`nBton*e1k^qG{uf!OTOot`^+JI`4QbRnr!Ey+sM_ zRV%vz;~SpQ2}!VR{1qP%4&i*a9MoNI> zjlCInsp(2i2EYuDgqI?&KZKVo%G(;@m6X>=K3Ah|sT}0>om$eWUON+8*SX~9t;Wn( zY()PjU}iHrKJ=1>eXB%2xoj0C!Lc^vZl&*Gb^hhwtEcU_&GZP4)+AFyh5rqhJ$#*S zo422FcirKrT>EZa=KjfQhLbgS^oQssZB$-|00ePUCI|v!XR_L}rT-Z)8()Hu_1$QE2 zVKlTFyE;)~Fj6^OxF33ufRRU9nP?&PI5LErwR-vsFBnKmxAh%W6fL?lL3~VS>nY71 z=gnz)*w;m4ZC%viEpTc5Hj^ZFM>Z;39KuhtUrW|#ZtZk9=4YK9dt-{H{P8bk5hAIH zoO&6q%b(9SAg&ff&%f+AH6*bz+J8n#O9Gx9eB*C2j24QfM2ieto~T%9!MSoTCc-_A|+h;h((Ym;Y+s8w$PQ3mq`QcL3xG-`bu>{6B^#2I)9KhSaueY6 z0cJ9JTOi%1an-hUeoMX*{DU~X;k7J5ScS;rf;wysh2RcbFgOTAc z!2#)BKP8E2z;?j4JdrnQUUBI?HlV%BqY5decf`uT7M{0nM7JhshBRE@g%oD<5!UF4Y6 z3An!1WG2Ooc_IRQ%IauS7vD<3n~r?9vZY;B0J@lX(cr`p;8NB3rAf0TU!*{&{a7nd6Tuq_N&R2)@EGRmf@J9!6i$QDU!IsD4OIjodk?A7q9`tu*>9bi<){zF}V=A0$q zB4TA~1HE>Nk9RdLGHgHDWZaA4Or2LOy_I@79q@y@GWxcX`)>f?)2+msiO-;wxjY2qcN_KJdYE_90$V4^Q?3AnIv+S^HK_PA>$ zN{XIcOmhu<>g7Lqy3}%)H{Dg7eLUCj^WBw471A`{qgdbeqG7@`e5^Xxv&+;qk#dpg zYi6X+14Mw`LyGEZx>^IcD>CK^y3Re)Yxpsi-dhS&gi})*<%#Mx@N=|d?fpFdY1)ac z-MF}3P#@h+*L4Z@&$sfDXaE_I@%yV0U6}=i^op9a%#hr-AuefK&BtHOs;7H)jvR_l zw_;pa1`T7z0U=z`V1)FJ$^o@Fp<92i*sFU+6o+S}&wIE?m_*@zrKEBi9cDPsDW{y7P@_Fa_WosQeL^L)@dLIK4Uf zBDFxoezXR{uw$v`g1r4ZU2(9Ts< znybJj>B}V=&{wxK0~Bkt#U2`P?K%+blg>H^VAtWQrWI{3)c2B{UlbrhfT;p+0RXn8 zf3*PdI^RAGH&L{ac^P$>giE+A=9cObHUbGWS!oOhAv zMb8TmUM2<5v(>}`;vJb#2|o4Bf!W?k8_QhH_(uFx&=w4aD>LmL%M{E|e748B(70dK zD9krokGH_i)>{Fp)eMBUEt@zu_)ikNb#X43K8xw+XEmLTHh+EWh_Du~n zbu?Q@rK`WCH}rn9e@%1eFA$&KE$4HoKQ_S($s$5kjjv*jFZ~*DRKaGC7a_d-J~qA_X&u%Hz59vUwfqz% z0%x)OU;3|se{ZsdL2nNV(4FnSZ`ApF$xN<1g%>4_#t(A&Ju$7rmuT2vaoe)D+Od%-$2u7gY>VrA3KG_dHyUtWFq8U-dW347~I@XwPv;-iUw zYt^xgN;BrVWHZlpN@@py`>PfPQhb|l>hs(fA4^||BtI7V8(=yy(%X_@>SoS17V$R> z*5AD?6~7|KyVB6OmZrfPVrEcYElYg;vv|SG*Atil?s;*P_Wdg0xn%uDjuYb28|>S~ zX^D@HF^xf;5eqp)e8Bd<5FAYOT3rCXzQhDZ>-?-9$+8|e<;f;|Ud9GKKk^p}E|4M> z7%;vv7{p_&)*2cdV#3_Zqk#0UM-PdCI0ynF_2wbec)5zH!GhiM@9K6Te;iZ8jG=;$~0GLfwzOOg}#X>!lku3Qqb2AFYdt zi=gU1^c{b);1MA0&Geok@OaPJ_c&hqpcm0=icX2}b`7Z7I`MgTdO@LS&_gdTb9-d3 zT2yvh5XG6fe7FJRA^8}B2upW{ta#1!t6AQe&FE=0wjC1?ccZ8rG?PhY~2wBT*kTO+Fy&WpEhCJPT6 zd%az%qc~1!Z__x~BHCYLn8U@D!^?gcLsJT}Galzw;e{2kD&Er+r?#Y=*dD9wg1+tz zW=!zAx(auGpNLoDLYOHd#k7S7FI>`NoE*U%e(Uza(9g1Op5!#9>#si$@Xfa7yDVKj zZI3315fthTmROI5t5r=&Saf^YKBAMEVyBG+rZamix&69eIEevoYt9bDiWRKan*<{bZGNZqLf|&&*LpI3i1YGUw1Y856EwM~0FAqJt_9k%ApEw~> zB&-nERe!N<#cOhXQAE*ha(rn!m6v=^Y#tqQn`u|q8G!VACn^L)jQG_M2ao{xSP`np zUJB*>M}GtuXhAF+@>*l2o6NH!UQ#cQnEUj8u4b%hJKyD;cJIWzfi)!334>IMP)GZ4 zsTwEDQ3Ql{lx(3As7;KKX@{&_NtA14JhL9l@R*{v%;)_$He;2~sUM~IW$nAzx~oC@`OPNk`-bWm)s&%7W4&%0ir$3L2TE zrV*8q z=io6I%#5J5{u_X7L%z7F;J1=fRSX1HKo1JXfsY6mmx009cm?^>!uJ*poi<4OD2~>r zGhBvUv|~P3269$gHB~*0oL_mx-oW1qbza>s_KVhstF|nvU9X;8S$ELvtGQ|P$-}?9 z9HkFGsA~_$HeVuM5@~*5+p?*=B`NQEJ+-GSk{0x^Z*Y!;U1osGTI?mQj}d)3-yJ9G z^u<+%Zt4%UbDo)C7oqU^t#^(@``(LDwXcHzo!*VS=oWoTUb6>5HcM9E&Uh-)^wB-T zlORWlwsnp_B!nFZUb2GQldF10bhGA`RG-XC%?sf?^xJlpDEX9(myOrQ8_}C=&007! zCxjy!&(_4FTK(H>O~!leWwxda)rju`dHtgSZEj@S1Iooh%e;OELYUmoJ7wHz{? zyF7NYYuYBx^gT@BEnn>@m$&3LFp7R+e(XS4QzWMz1yDWe&7ub?S_@t| zsKN#%sjwQipZR+2UC&ZZ6unqZEeB6G!z}qOJ;L&VhJCgCH#Z4{=w&LlR11pUKDG_! zsKLQfa4?j>>y?pb8+|%2_n)79@hCBlY9VE|-hjiTA!8o6EEH8*Y@BC|M1;2@g@ML| z+08zM43M~S?=_q~2-1B%d2YWqdPc%!DFZU10HYnTt;t6l$*9>Hh+~OLZFD7gRuGvr zhw8ZkcZOahbzSU}2N=(=n;q%WgH9DKR>YQYkVevC-N7V^8$-d0us*B+>jH0uxN*e( z%(Tw-nEXa<27z*v@k2xEOX_ad0N|7H5{%9Oyu(OPejKf>-N!v&G9#?1Y^2W`8_ zaT=p|)G!9M(2pra<058Fbyf^$lUfA=*XSAT=7|DXK_g|XdH!GDvXH6R>2AZYwVra8 zNQ4}*8T_67*9nu(*mR(PnZs%uW`;r$Z3@@buMQm28R*i45o0}tn@!Xin9_)qZca8A zL+^uS=|PZIf|p%-@KX3S4f-<%v`N>Lt0Wj*DMG?J$XVq?i@g^xauU|hO&4QEHr^nD z*~hSKrO4>_^qlZ6A>yqhn;MGvlol(ER}2>|y~#6X2gnOoD$)VZvjHt0)QTGiVyN(S zH5-W_3@K?5qsT(wd>P+8Js}4(o|_tZ?{89J7#UE7A!>(f0Wp%`LZ)RniwqWuLxH1^a?U)*%AVpPYEHxAn6^Rtt%W}`>_q*ek9cCzmxdk0 zxWR*cv3Fc}e;({U+cb;mx|E|YKW#p?=Yb|*4lXwc($Fk#+!6(xP4ZbdUvFg&& zq7qVncvgi?He%v&9_4q?4~2Mg^*93tA<;D42T+)?yn$#GH-Hy^h*C#M3kBo?k?u+h z&*L92=NgG3Ir8gIn=cBX_Ml0&EJob3OTvj3=92||S9~hMVU&-vQobJm;_5U%He*}FB1@w9bVAR2%GqVXzaRlm39dTF)v6Zn8$?E z9_MeKBOv4MveX_GPEw2~|B9ukqqPhZO}|B;*6;Q1gF(tr@BU2oHTM0wbNAA_^5-v` z>f%-4$v#!a_pSChcwmQr-}Eb++3p`ka2CY+D&3Fqkyv$z6k73)$9-tO1pc+hBCB}~ z3g`+Sa~`7@Pxyh8glMuus`xjM#LFBmYrno&D3J#FDTCCH#k`3~XkJ4>Ybt^WfmOT&oWCBdCj;fju z8qKagQ}@`Bg3oSdtC0^)hIn@S-h!admocjkUC^$(3V-dne%r98I9XaK(GmqbOOOne za>!E<#GvVALj}2>yxl)-%-j%X$MnzJnw9Nd3&KPWz0mq>?@!bi{E&s}e6I=EBHrdS z3{ETn1`6mbm+$0o;AE|(TMRY!anaO=2f^FCQ!sL+vdM*4zj;+(LCR;o>lQ3qWcBXP z-INTOi>Zt~vs1kWEvqEU-MLcDO?Q%()_&ne$FX2oQDnD=)d7fpPKoeavWpkZ9$kNg|f`pc3G(Vwb5vm<+ zK9&*=O{PpxIxWrKB^6RK4oc~fIw`(E^!CxoJg!-cL1#l5); z^1jvpWSFDk2hTU?G(Ws>x?2SB`-D-Ai-zU`bW^S-PhK<^7ph;ph z`4dz7GNO+nd_HK5)K*T=oDm1@+#;;Mwm2OP@(97u z^l-`t$l#|kvIwcrFxS;CwgGvChTXq+*WCJ|m;2G6QzpR8*-7AAvMcw{U73$=fXQZe zM@Tw8+u!5{f`f|cv-=hZ#xLUn=Wz2RbVhUy0|#83D6ex?k{&w3Q|bVDpUR~~AIN+3 zLz1r=RE5UQybKES=QC>TqC0qGyQPCr{OatzD)*d~6z5F;760Dd1F05sj(MlYC|r=G zr;UYisaoV2G2{<-+no!|`w7Vn6m{(M@aTy@Gpe6Qi#8I8yE=UbW)~?Temts5p0+jE z;cQQt-&fp2XqwzdEo}7Yoz{y&$jmQpFM5jo=90&D$+KAtCzOE}e%(N9scbB0Af^XS zul3#@yg_t3EG6F?Bzh(cLurw4k?t zpxW$wfQfH$?Q+_K2M!6Z_t0~)Tar@xA||f=-pTmnltwOVpd>4O8g$pu2Q9NwDeNY+xC%NuX9980 z$;)fKHcPgrqZZR60!+6qRa73(1=;HPkqY|f2i_1M$T%i=tdeZ9fhM1<@^a5}dPVW3 z@Q83Ybnp8;^3nQJVffXqE==4jbu>36x`2CQL-cU5VO0rgJbAgG{H(bFch=krm^tAO zOq2+QTMP2vJ~{%U^zWQp^)!%ViIO7C|K|kw!S*<&)8h(-bW~%{M^!wTMY|Myd}%gB z%>4$F97D%-8P_sjp&gi?F1Gh%Kw13^C4?GZpB z^@0&nd(4Wnimtzgk6#q9gNVw^XeTTREur@5b+KX5^sr=6LkOE3CECN

GN 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 C-k?7K literal 0 HcmV?d00001 From b0972c86429d01a5d32ac207cf89208d269274bf Mon Sep 17 00:00:00 2001 From: Sattar Imamov Date: Tue, 15 Jun 2021 00:12:03 +0200 Subject: [PATCH 12/26] fixes RemoteWidget error when width or height is equal to zero. Issue #6068 --- src/Avalonia.Controls/Remote/RemoteWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index fabb38f87d..234960e87c 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -70,7 +70,7 @@ namespace Avalonia.Controls.Remote public override void Render(DrawingContext context) { - if (_lastFrame != null) + if (_lastFrame != null && _lastFrame.Width != 0 && _lastFrame.Height != 0) { var fmt = (PixelFormat) _lastFrame.Format; if (_bitmap == null || _bitmap.PixelSize.Width != _lastFrame.Width || From c8b09d4156aa5d94aa7f490ec7155d3b1af460d5 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 15 Jun 2021 09:43:10 +0200 Subject: [PATCH 13/26] Update API compat. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index a79b3b4d7b..46c12ebd39 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -4,6 +4,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalon InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. @@ -11,4 +12,4 @@ EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -Total Issues: 12 +Total Issues: 13 From c7da3ccdc555d18910635102c485747d45bb2bc0 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 15 Jun 2021 16:23:52 +0200 Subject: [PATCH 14/26] feat(DevTools): Startup Screen --- samples/ControlCatalog/MainWindow.xaml.cs | 5 ++++- .../Diagnostics/DevTools.cs | 4 +++- .../Diagnostics/DevToolsOptions.cs | 5 +++++ .../Diagnostics/ViewModels/MainViewModel.cs | 9 ++++++++- .../Diagnostics/Views/MainWindow.xaml.cs | 20 +++++++++++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 723351ae57..cd55ad78a7 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -17,7 +17,10 @@ namespace ControlCatalog public MainWindow() { this.InitializeComponent(); - this.AttachDevTools(); + this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() + { + StartupScreen = 1, + }); //Renderer.DrawFps = true; //Renderer.DrawDirtyRects = Renderer.DrawFps = true; diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 0e36c8f9cb..2a386f106e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -10,7 +10,8 @@ namespace Avalonia.Diagnostics { public static class DevTools { - private static readonly Dictionary s_open = new Dictionary(); + private static readonly Dictionary s_open = + new Dictionary(); public static IDisposable Attach(TopLevel root, KeyGesture gesture) { @@ -52,6 +53,7 @@ namespace Avalonia.Diagnostics Width = options.Size.Width, Height = options.Size.Height, }; + window.SetOptions(options); window.Closed += DevToolsClosed; s_open.Add(root, window); diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index f9978f3b7e..4677f1ef9b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -22,5 +22,10 @@ namespace Avalonia.Diagnostics /// Gets or sets the initial size of the DevTools window. The default value is 1280x720. /// public Size Size { get; set; } = new Size(1280, 720); + + ///

+ /// Get or set the startup screen number where the DevTools window will be displayed. + /// + public int? StartupScreen { get; set; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 72491bebc2..bf35641b40 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -151,7 +151,7 @@ namespace Avalonia.Diagnostics.ViewModels get { return _pointerOverElement; } private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } } - + private void UpdateConsoleContext(ConsoleContext context) { context.root = _root; @@ -213,5 +213,12 @@ namespace Avalonia.Diagnostics.ViewModels tree.SelectControl(control); } } + + public int? StartupScreenIndex { get; private set; } = default; + + public void SetOptions(DevToolsOptions options) + { + StartupScreenIndex = options.StartupScreen; + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index bbb8e76551..9639386ddf 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -24,6 +24,23 @@ namespace Avalonia.Diagnostics.Views _keySubscription = InputManager.Instance.Process .OfType() .Subscribe(RawKeyDown); + + EventHandler? lh = default; + lh = (s, e) => + { + this.Opened -= lh; + if ((DataContext as MainViewModel)?.StartupScreenIndex is int index) + { + var screens = this.Screens; + if (index < screens.ScreenCount) + { + var screen = screens.All[index]; + this.Position = screen.Bounds.TopLeft; + this.WindowState = WindowState.Maximized; + } + } + }; + this.Opened += lh; } public TopLevel? Root @@ -115,5 +132,8 @@ namespace Avalonia.Diagnostics.Views } private void RootClosed(object? sender, EventArgs e) => Close(); + + public void SetOptions(DevToolsOptions options) => + (DataContext as MainViewModel)?.SetOptions(options); } } From fa133444c4104379c2de4229200355f2cd018e83 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 15 Jun 2021 18:16:03 +0200 Subject: [PATCH 15/26] fixes: Renamed StartupScreen to StartupScreenIndex --- samples/ControlCatalog/MainWindow.xaml.cs | 2 +- src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs | 4 ++-- .../Diagnostics/ViewModels/MainViewModel.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index cd55ad78a7..2446c0e1c9 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -19,7 +19,7 @@ namespace ControlCatalog this.InitializeComponent(); this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() { - StartupScreen = 1, + StartupScreenIndex = 1, }); //Renderer.DrawFps = true; //Renderer.DrawDirtyRects = Renderer.DrawFps = true; diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index 4677f1ef9b..5336dca65b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -24,8 +24,8 @@ namespace Avalonia.Diagnostics public Size Size { get; set; } = new Size(1280, 720); /// - /// Get or set the startup screen number where the DevTools window will be displayed. + /// Get or set the startup screen index where the DevTools window will be displayed. /// - public int? StartupScreen { get; set; } + public int? StartupScreenIndex { get; set; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index bf35641b40..3f367165ac 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -218,7 +218,7 @@ namespace Avalonia.Diagnostics.ViewModels public void SetOptions(DevToolsOptions options) { - StartupScreenIndex = options.StartupScreen; + StartupScreenIndex = options.StartupScreenIndex; } } } From 02fd0006a9ddd11f88e3e7a821d779aa46c35e0d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 15 Jun 2021 18:19:55 +0200 Subject: [PATCH 16/26] fixes: check StartupScreenIndex is greater than -1 --- src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 9639386ddf..d1232b749a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.Views if ((DataContext as MainViewModel)?.StartupScreenIndex is int index) { var screens = this.Screens; - if (index < screens.ScreenCount) + if (index > -1 && index < screens.ScreenCount) { var screen = screens.All[index]; this.Position = screen.Bounds.TopLeft; From 8f203c6800c3d5cc2e5b9332c7376574e1fa4747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 15 Jun 2021 21:57:25 +0200 Subject: [PATCH 17/26] Add Animator property to Setter --- src/Avalonia.Animation/Animation.cs | 2 +- src/Avalonia.Animation/IAnimationSetter.cs | 3 +++ src/Avalonia.Styling/Styling/Setter.cs | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index c42153ec4f..07b8c1a54e 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -248,7 +248,7 @@ namespace Avalonia.Animation { foreach (var setter in keyframe.Setters) { - var handler = GetAnimatorType(setter.Property); + var handler = setter.Animator ?? GetAnimatorType(setter.Property); if (handler == null) { diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index 2d22377286..072d7096ae 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -1,8 +1,11 @@ +using System; + namespace Avalonia.Animation { public interface IAnimationSetter { AvaloniaProperty Property { get; set; } object Value { get; set; } + Type Animator { get; set; } } } diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 1f3d6335a9..114a97dbb6 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -59,6 +59,11 @@ namespace Avalonia.Styling } } + /// + /// Gets or sets the property animator. + /// + public Type? Animator { get; set; } + public ISetterInstance Instance(IStyleable target) { target = target ?? throw new ArgumentNullException(nameof(target)); From d323d069df0747a60cd5f13cbf67899ab3fbd4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 15 Jun 2021 22:07:45 +0200 Subject: [PATCH 18/26] Create ApiCompatBaseline.txt --- src/Avalonia.Animation/ApiCompatBaseline.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/Avalonia.Animation/ApiCompatBaseline.txt diff --git a/src/Avalonia.Animation/ApiCompatBaseline.txt b/src/Avalonia.Animation/ApiCompatBaseline.txt new file mode 100644 index 0000000000..69b9dc5c77 --- /dev/null +++ b/src/Avalonia.Animation/ApiCompatBaseline.txt @@ -0,0 +1,5 @@ +Compat issues with assembly Avalonia.Animation: +InterfacesShouldHaveSameMembers : Interface member 'public System.Type Avalonia.Animation.IAnimationSetter.Animator' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Type Avalonia.Animation.IAnimationSetter.Animator.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Animation.IAnimationSetter.Animator.set(System.Type)' is present in the implementation but not in the contract. +Total Issues: 3 From 47482445643b70547ad55864d8289312a730f8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 16 Jun 2021 13:21:32 +0200 Subject: [PATCH 19/26] Add custom animator example --- samples/RenderDemo/MainWindow.xaml | 3 +++ .../RenderDemo/Pages/CustomAnimatorPage.xaml | 25 +++++++++++++++++ .../Pages/CustomAnimatorPage.xaml.cs | 27 +++++++++++++++++++ .../RenderDemo/Pages/CustomStringAnimator.cs | 16 +++++++++++ 4 files changed, 71 insertions(+) create mode 100644 samples/RenderDemo/Pages/CustomAnimatorPage.xaml create mode 100644 samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs create mode 100644 samples/RenderDemo/Pages/CustomStringAnimator.cs diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index aa165d13f7..f37df56b73 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -36,6 +36,9 @@ + + + diff --git a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml new file mode 100644 index 0000000000..6abb9a1217 --- /dev/null +++ b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs new file mode 100644 index 0000000000..eed8ee29ce --- /dev/null +++ b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs @@ -0,0 +1,27 @@ +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using RenderDemo.ViewModels; + +namespace RenderDemo.Pages +{ + public class CustomAnimatorPage : UserControl + { + public CustomAnimatorPage() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/RenderDemo/Pages/CustomStringAnimator.cs b/samples/RenderDemo/Pages/CustomStringAnimator.cs new file mode 100644 index 0000000000..851a2d0187 --- /dev/null +++ b/samples/RenderDemo/Pages/CustomStringAnimator.cs @@ -0,0 +1,16 @@ +using Avalonia.Animation.Animators; + +namespace RenderDemo.Pages +{ + public class CustomStringAnimator : Animator + { + public override string Interpolate(double progress, string oldValue, string newValue) + { + if (newValue.Length == 0) return ""; + var step = 1.0 / newValue.Length; + var length = (int)(progress / step); + var result = newValue.Substring(0, length + 1); + return result; + } + } +} From 84dd710fc184c322a59f4f8a650d5573f2ba18de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 16 Jun 2021 13:53:35 +0200 Subject: [PATCH 20/26] Use attached property --- .../RenderDemo/Pages/CustomAnimatorPage.xaml | 4 ++-- src/Avalonia.Animation/Animation.cs | 18 +++++++++++++++++- src/Avalonia.Animation/IAnimationSetter.cs | 1 - src/Avalonia.Styling/Styling/Setter.cs | 5 ----- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml index 6abb9a1217..b386636cae 100644 --- a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml +++ b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml @@ -11,10 +11,10 @@ - + - + diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 07b8c1a54e..76fbfafaa5 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -194,6 +194,22 @@ namespace Avalonia.Animation [Content] public KeyFrames Children { get; } = new KeyFrames(); + private static readonly Dictionary s_animators = new Dictionary(); + + public static Type GetAnimator(object obj) + { + if (s_animators.TryGetValue(obj, out var type)) + { + return type; + } + return null; + } + + public static void SetAnimator(object obj, Type value) + { + s_animators[obj] = value; + } + private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> { ( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ), @@ -248,7 +264,7 @@ namespace Avalonia.Animation { foreach (var setter in keyframe.Setters) { - var handler = setter.Animator ?? GetAnimatorType(setter.Property); + var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property); if (handler == null) { diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index 072d7096ae..d916f19370 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -6,6 +6,5 @@ namespace Avalonia.Animation { AvaloniaProperty Property { get; set; } object Value { get; set; } - Type Animator { get; set; } } } diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 114a97dbb6..1f3d6335a9 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -59,11 +59,6 @@ namespace Avalonia.Styling } } - /// - /// Gets or sets the property animator. - /// - public Type? Animator { get; set; } - public ISetterInstance Instance(IStyleable target) { target = target ?? throw new ArgumentNullException(nameof(target)); From 65b115c6e17321f45bf8b972b20ce005f5531284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 16 Jun 2021 14:03:33 +0200 Subject: [PATCH 21/26] Add comments --- src/Avalonia.Animation/Animation.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 76fbfafaa5..8571d49b42 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -194,8 +194,14 @@ namespace Avalonia.Animation [Content] public KeyFrames Children { get; } = new KeyFrames(); + // Store values for the Animator attached properties. private static readonly Dictionary s_animators = new Dictionary(); + /// + /// Gets the value of the Animator attached property for an object. + /// + /// The object. + /// The property animator type. public static Type GetAnimator(object obj) { if (s_animators.TryGetValue(obj, out var type)) @@ -205,6 +211,11 @@ namespace Avalonia.Animation return null; } + /// + /// Sets the value of the Animator attached property for an object. + /// + /// The object. + /// The property animator value. public static void SetAnimator(object obj, Type value) { s_animators[obj] = value; From f7cc89c86bba426b7449c3bab53d0976390ead94 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 16 Jun 2021 16:01:36 +0100 Subject: [PATCH 22/26] fix osx window state logic. --- native/Avalonia.Native/src/OSX/window.mm | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 870345e543..c0936356d2 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -513,6 +513,7 @@ private: bool _fullScreenActive; SystemDecorations _decorations; AvnWindowState _lastWindowState; + AvnWindowState _actualWindowState; bool _inSetWindowState; NSRect _preZoomSize; bool _transitioningWindowState; @@ -539,6 +540,7 @@ private: _transitioningWindowState = false; _inSetWindowState = false; _lastWindowState = Normal; + _actualWindowState = Normal; WindowEvents = events; [Window setCanBecomeKeyAndMain]; [Window disableCursorRects]; @@ -633,7 +635,7 @@ private: void WindowStateChanged () override { - if(!_inSetWindowState && !_transitioningWindowState) + if(_shown && !_inSetWindowState && !_transitioningWindowState) { AvnWindowState state; GetWindowState(&state); @@ -963,14 +965,14 @@ private: { @autoreleasepool { - if(_lastWindowState == state) + if(_actualWindowState == state) { return S_OK; } _inSetWindowState = true; - auto currentState = _lastWindowState; + auto currentState = _actualWindowState; _lastWindowState = state; if(currentState == Normal) @@ -1049,8 +1051,11 @@ private: } break; } + + _actualWindowState = _lastWindowState; } + _inSetWindowState = false; return S_OK; From 42a7c835fb24bba53508d220b88f361ea0213b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 17 Jun 2021 06:52:18 +0200 Subject: [PATCH 23/26] Delete ApiCompatBaseline.txt --- src/Avalonia.Animation/ApiCompatBaseline.txt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 src/Avalonia.Animation/ApiCompatBaseline.txt diff --git a/src/Avalonia.Animation/ApiCompatBaseline.txt b/src/Avalonia.Animation/ApiCompatBaseline.txt deleted file mode 100644 index 69b9dc5c77..0000000000 --- a/src/Avalonia.Animation/ApiCompatBaseline.txt +++ /dev/null @@ -1,5 +0,0 @@ -Compat issues with assembly Avalonia.Animation: -InterfacesShouldHaveSameMembers : Interface member 'public System.Type Avalonia.Animation.IAnimationSetter.Animator' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Type Avalonia.Animation.IAnimationSetter.Animator.get()' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Animation.IAnimationSetter.Animator.set(System.Type)' is present in the implementation but not in the contract. -Total Issues: 3 From 43bfff83a048ff78fe6989f3b874bf048605faba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 17 Jun 2021 06:52:45 +0200 Subject: [PATCH 24/26] Update IAnimationSetter.cs --- src/Avalonia.Animation/IAnimationSetter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index d916f19370..2d22377286 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -1,5 +1,3 @@ -using System; - namespace Avalonia.Animation { public interface IAnimationSetter From 76d8bcff56a3e06bd3f74c433ea6da930208462e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 17 Jun 2021 07:08:56 +0200 Subject: [PATCH 25/26] Use IAnimationSetter instead of object to attached property --- src/Avalonia.Animation/Animation.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 8571d49b42..b315b65154 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -194,17 +194,17 @@ namespace Avalonia.Animation [Content] public KeyFrames Children { get; } = new KeyFrames(); - // Store values for the Animator attached properties. - private static readonly Dictionary s_animators = new Dictionary(); + // Store values for the Animator attached properties for IAnimationSetter objects. + private static readonly Dictionary s_animators = new Dictionary(); /// - /// Gets the value of the Animator attached property for an object. + /// Gets the value of the Animator attached property for a setter. /// - /// The object. + /// The animation setter. /// The property animator type. - public static Type GetAnimator(object obj) + public static Type GetAnimator(IAnimationSetter setter) { - if (s_animators.TryGetValue(obj, out var type)) + if (s_animators.TryGetValue(setter, out var type)) { return type; } @@ -212,11 +212,11 @@ namespace Avalonia.Animation } /// - /// Sets the value of the Animator attached property for an object. + /// Sets the value of the Animator attached property for a setter. /// - /// The object. + /// The animation setter. /// The property animator value. - public static void SetAnimator(object obj, Type value) + public static void SetAnimator(IAnimationSetter setter, Type value) { s_animators[obj] = value; } From ab28847bc6e0f7660a2455e735d0ba961696b2ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 17 Jun 2021 08:07:41 +0200 Subject: [PATCH 26/26] Fix --- src/Avalonia.Animation/Animation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index b315b65154..daa4793ef0 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -218,7 +218,7 @@ namespace Avalonia.Animation /// The property animator value. public static void SetAnimator(IAnimationSetter setter, Type value) { - s_animators[obj] = value; + s_animators[setter] = value; } private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)>