From 3304d646c007d3cbe72784bde5227ed76133d377 Mon Sep 17 00:00:00 2001 From: Fusion86 Date: Sun, 15 Nov 2020 18:49:23 +0100 Subject: [PATCH 001/184] 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 002/184] 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 003/184] 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 b96df46054014da28b6ec7f3526b9aa2238882c5 Mon Sep 17 00:00:00 2001 From: Il Harper Date: Sat, 1 May 2021 23:56:50 +0800 Subject: [PATCH 004/184] fix: Fix ArgumentNullException in TreeView.ExpandSubTree() close #4288 --- src/Avalonia.Controls/TreeView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 3afbbd944c..e890e4346f 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -168,7 +168,7 @@ namespace Avalonia.Controls var panel = item.Presenter.Panel; - if (panel != null) + if (item.Presenter?.Panel != null) { foreach (var child in panel.Children) { From c6464ba8d55f4713e89de2c2784959c2f740c06f Mon Sep 17 00:00:00 2001 From: Il Harper Date: Thu, 13 May 2021 21:59:34 +0800 Subject: [PATCH 005/184] fix: Refactor --- src/Avalonia.Controls/TreeView.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index e890e4346f..78cd22ae32 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -166,11 +166,9 @@ namespace Avalonia.Controls { item.IsExpanded = true; - var panel = item.Presenter.Panel; - if (item.Presenter?.Panel != null) { - foreach (var child in panel.Children) + foreach (var child in item.Presenter.Panel.Children) { if (child is TreeViewItem treeViewItem) { From 2956f79c5c8268dd4d3f763324a501336e756394 Mon Sep 17 00:00:00 2001 From: Scott Whitney Date: Thu, 27 May 2021 21:52:37 +0930 Subject: [PATCH 006/184] Added DecimalPlaces property to NumericUpDown control --- .../NumericUpDown/NumericUpDown.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index abfbc038eb..e3db619269 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -119,6 +119,12 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty DecimalPlacesProperty = + AvaloniaProperty.Register(nameof(DecimalPlaces), 2); + private IDisposable _textBoxTextChangedSubscription; private double _value; @@ -261,6 +267,15 @@ namespace Avalonia.Controls } } + /// + /// Gets or sets the number of decimal places to display in the up-down control. + /// + public int DecimalPlaces + { + get { return GetValue(DecimalPlacesProperty); } + set { SetValue(DecimalPlacesProperty, value); } + } + /// /// Gets or sets the object to use as a watermark if the is null. /// @@ -620,7 +635,7 @@ namespace Avalonia.Controls /// private void OnIncrement() { - var result = Value + Increment; + var result = Math.Round(Value + Increment, DecimalPlaces); Value = MathUtilities.Clamp(result, Minimum, Maximum); } @@ -629,7 +644,7 @@ namespace Avalonia.Controls /// private void OnDecrement() { - var result = Value - Increment; + var result = Math.Round(Value - Increment, DecimalPlaces); Value = MathUtilities.Clamp(result, Minimum, Maximum); } From 4cc7dba01a98e4781544b78811188614c0d9da0d Mon Sep 17 00:00:00 2001 From: Scott Whitney Date: Fri, 28 May 2021 10:21:15 +0930 Subject: [PATCH 007/184] Changed backing types to decimal. Added NumberFormat property. Changed ThrowCannotBeGreaterThanException to accept generics as it displays the values only. --- src/Avalonia.Base/Utilities/MathUtilities.cs | 32 +++- .../NumericUpDown/NumericUpDown.cs | 165 +++++++++++------- .../NumericUpDownValueChangedEventArgs.cs | 6 +- 3 files changed, 132 insertions(+), 71 deletions(-) diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 1ea369954a..596cbf1d7e 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -224,6 +224,34 @@ namespace Avalonia.Utilities } } + /// + /// Clamps a value between a minimum and maximum value. + /// + /// The value. + /// The minimum value. + /// The maximum value. + /// The clamped value. + public static decimal Clamp(decimal val, decimal min, decimal max) + { + if (min > max) + { + ThrowCannotBeGreaterThanException(min, max); + } + + if (val < min) + { + return min; + } + else if (val > max) + { + return max; + } + else + { + return val; + } + } + /// /// Clamps a value between a minimum and maximum value. /// @@ -281,8 +309,8 @@ namespace Avalonia.Utilities { return angle * 2 * Math.PI; } - - private static void ThrowCannotBeGreaterThanException(double min, double max) + + private static void ThrowCannotBeGreaterThanException(T min, T max) { throw new ArgumentException($"{min} cannot be greater than {max}."); } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index e3db619269..659bc272e0 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -49,6 +49,13 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect(nameof(CultureInfo), o => o.CultureInfo, (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); + /// + /// Defines the property. + /// + public static readonly DirectProperty NumberFormatProperty = + AvaloniaProperty.RegisterDirect(nameof(NumberFormat), o => o.NumberFormat, + (o, v) => o.NumberFormat = v, NumberFormatInfo.CurrentInfo); + /// /// Defines the property. /// @@ -58,8 +65,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty IncrementProperty = - AvaloniaProperty.Register(nameof(Increment), 1.0d, coerce: OnCoerceIncrement); + public static readonly StyledProperty IncrementProperty = + AvaloniaProperty.Register(nameof(Increment), 1.0m, coerce: OnCoerceIncrement); /// /// Defines the property. @@ -70,14 +77,14 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty MaximumProperty = - AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, coerce: OnCoerceMaximum); + public static readonly StyledProperty MaximumProperty = + AvaloniaProperty.Register(nameof(Maximum), decimal.MaxValue, coerce: OnCoerceMaximum); /// /// Defines the property. /// - public static readonly StyledProperty MinimumProperty = - AvaloniaProperty.Register(nameof(Minimum), double.MinValue, coerce: OnCoerceMinimum); + public static readonly StyledProperty MinimumProperty = + AvaloniaProperty.Register(nameof(Minimum), decimal.MinValue, coerce: OnCoerceMinimum); /// /// Defines the property. @@ -96,8 +103,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// @@ -106,7 +113,6 @@ namespace Avalonia.Controls public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register(nameof(Watermark)); - /// /// Defines the property. /// @@ -119,15 +125,9 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly StyledProperty DecimalPlacesProperty = - AvaloniaProperty.Register(nameof(DecimalPlaces), 2); - private IDisposable _textBoxTextChangedSubscription; - private double _value; + private decimal _value; private string _text; private bool _internalValueSet; private bool _clipValueToMinMax; @@ -135,7 +135,8 @@ namespace Avalonia.Controls private bool _isTextChangedFromUI; private CultureInfo _cultureInfo; private NumberStyles _parsingNumberStyle = NumberStyles.Any; - + private NumberFormatInfo _numberFormat; + /// /// Gets the Spinner template part. /// @@ -188,7 +189,21 @@ namespace Avalonia.Controls public CultureInfo CultureInfo { get { return _cultureInfo; } - set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); } + set + { + SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); + //Set and Raise the NumberFormatProperty when CultureInfo is changed. + SetAndRaise(NumberFormatProperty, ref _numberFormat, value?.NumberFormat); + } + } + + /// + /// Gets or sets the current NumberFormatInfo + /// + public NumberFormatInfo NumberFormat + { + get { return _numberFormat; } + set { SetAndRaise(NumberFormatProperty, ref _numberFormat, value); } } /// @@ -203,7 +218,7 @@ namespace Avalonia.Controls /// /// Gets or sets the amount in which to increment the . /// - public double Increment + public decimal Increment { get { return GetValue(IncrementProperty); } set { SetValue(IncrementProperty, value); } @@ -221,7 +236,7 @@ namespace Avalonia.Controls /// /// Gets or sets the maximum allowed value. /// - public double Maximum + public decimal Maximum { get { return GetValue(MaximumProperty); } set { SetValue(MaximumProperty, value); } @@ -230,7 +245,7 @@ namespace Avalonia.Controls /// /// Gets or sets the minimum allowed value. /// - public double Minimum + public decimal Minimum { get { return GetValue(MinimumProperty); } set { SetValue(MinimumProperty, value); } @@ -257,7 +272,7 @@ namespace Avalonia.Controls /// /// Gets or sets the value. /// - public double Value + public decimal Value { get { return _value; } set @@ -267,15 +282,6 @@ namespace Avalonia.Controls } } - /// - /// Gets or sets the number of decimal places to display in the up-down control. - /// - public int DecimalPlaces - { - get { return GetValue(DecimalPlacesProperty); } - set { SetValue(DecimalPlacesProperty, value); } - } - /// /// Gets or sets the object to use as a watermark if the is null. /// @@ -285,7 +291,6 @@ namespace Avalonia.Controls set { SetValue(WatermarkProperty, value); } } - /// /// Gets or sets the horizontal alignment of the content within the control. /// @@ -326,6 +331,7 @@ namespace Avalonia.Controls static NumericUpDown() { CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged); + NumberFormatProperty.Changed.Subscribe(OnNumberFormatChanged); FormatStringProperty.Changed.Subscribe(FormatStringChanged); IncrementProperty.Changed.Subscribe(IncrementChanged); IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged); @@ -412,6 +418,19 @@ namespace Avalonia.Controls } } + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnNumberFormatChanged(NumberFormatInfo oldValue, NumberFormatInfo newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(false, null); + } + } + /// /// Called when the property value changed. /// @@ -430,7 +449,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnIncrementChanged(double oldValue, double newValue) + protected virtual void OnIncrementChanged(decimal oldValue, decimal newValue) { if (IsInitialized) { @@ -453,7 +472,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnMaximumChanged(double oldValue, double newValue) + protected virtual void OnMaximumChanged(decimal oldValue, decimal newValue) { if (IsInitialized) { @@ -470,7 +489,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnMinimumChanged(double oldValue, double newValue) + protected virtual void OnMinimumChanged(decimal oldValue, decimal newValue) { if (IsInitialized) { @@ -500,7 +519,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnValueChanged(double oldValue, double newValue) + protected virtual void OnValueChanged(decimal oldValue, decimal newValue) { if (!_internalValueSet && IsInitialized) { @@ -516,7 +535,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceIncrement(double baseValue) + protected virtual decimal OnCoerceIncrement(decimal baseValue) { return baseValue; } @@ -525,7 +544,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceMaximum(double baseValue) + protected virtual decimal OnCoerceMaximum(decimal baseValue) { return Math.Max(baseValue, Minimum); } @@ -534,7 +553,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceMinimum(double baseValue) + protected virtual decimal OnCoerceMinimum(decimal baseValue) { return Math.Min(baseValue, Maximum); } @@ -543,7 +562,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceValue(double baseValue) + protected virtual decimal OnCoerceValue(decimal baseValue) { return baseValue; } @@ -577,7 +596,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void RaiseValueChangedEvent(double oldValue, double newValue) + protected virtual void RaiseValueChangedEvent(decimal oldValue, decimal newValue) { var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); RaiseEvent(e); @@ -586,9 +605,9 @@ namespace Avalonia.Controls /// /// Converts the formatted text to a value. /// - private double ConvertTextToValue(string text) + private decimal ConvertTextToValue(string text) { - double result = 0; + decimal result = 0; if (string.IsNullOrEmpty(text)) { @@ -624,10 +643,10 @@ namespace Avalonia.Controls //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind. if (FormatString.Contains("{0")) { - return string.Format(CultureInfo, FormatString, Value); + return string.Format(NumberFormat, FormatString, Value); } - return Value.ToString(FormatString, CultureInfo); + return Value.ToString(FormatString, NumberFormat); } /// @@ -635,7 +654,7 @@ namespace Avalonia.Controls /// private void OnIncrement() { - var result = Math.Round(Value + Increment, DecimalPlaces); + var result = Value + Increment; Value = MathUtilities.Clamp(result, Minimum, Maximum); } @@ -644,7 +663,7 @@ namespace Avalonia.Controls /// private void OnDecrement() { - var result = Math.Round(Value - Increment, DecimalPlaces); + var result = Value - Increment; Value = MathUtilities.Clamp(result, Minimum, Maximum); } @@ -689,6 +708,20 @@ namespace Avalonia.Controls } } + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnNumberFormatChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (NumberFormatInfo)e.OldValue; + var newValue = (NumberFormatInfo)e.NewValue; + upDown.OnNumberFormatChanged(oldValue, newValue); + } + } + /// /// Called when the property value changed. /// @@ -697,8 +730,8 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnIncrementChanged(oldValue, newValue); } } @@ -739,8 +772,8 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnMaximumChanged(oldValue, newValue); } } @@ -753,8 +786,8 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnMinimumChanged(oldValue, newValue); } } @@ -781,13 +814,13 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnValueChanged(oldValue, newValue); } } - private void SetValueInternal(double value) + private void SetValueInternal(decimal value) { _internalValueSet = true; try @@ -800,7 +833,7 @@ namespace Avalonia.Controls } } - private static double OnCoerceMaximum(IAvaloniaObject instance, double value) + private static decimal OnCoerceMaximum(IAvaloniaObject instance, decimal value) { if (instance is NumericUpDown upDown) { @@ -810,7 +843,7 @@ namespace Avalonia.Controls return value; } - private static double OnCoerceMinimum(IAvaloniaObject instance, double value) + private static decimal OnCoerceMinimum(IAvaloniaObject instance, decimal value) { if (instance is NumericUpDown upDown) { @@ -820,7 +853,7 @@ namespace Avalonia.Controls return value; } - private static double OnCoerceIncrement(IAvaloniaObject instance, double value) + private static decimal OnCoerceIncrement(IAvaloniaObject instance, decimal value) { if (instance is NumericUpDown upDown) { @@ -992,23 +1025,23 @@ namespace Avalonia.Controls return parsedTextIsValid; } - private double ConvertTextToValueCore(string currentValueText, string text) + private decimal ConvertTextToValueCore(string currentValueText, string text) { - double result; + decimal result; if (IsPercent(FormatString)) { - result = decimal.ToDouble(ParsePercent(text, CultureInfo)); + result = ParsePercent(text, NumberFormat); } else { // Problem while converting new text - if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue)) + if (!decimal.TryParse(text, ParsingNumberStyle, NumberFormat, out var outputValue)) { var shouldThrow = true; // Check if CurrentValueText is also failing => it also contains special characters. ex : 90° - if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _)) + if (!decimal.TryParse(currentValueText, ParsingNumberStyle, NumberFormat, out var _)) { // extract non-digit characters var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); @@ -1021,7 +1054,7 @@ namespace Avalonia.Controls text = text.Replace(character.ToString(), string.Empty); } // if without the special characters, parsing is good, do not throw - if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue)) + if (decimal.TryParse(text, ParsingNumberStyle, NumberFormat, out outputValue)) { shouldThrow = false; } @@ -1038,7 +1071,7 @@ namespace Avalonia.Controls return result; } - private void ValidateMinMax(double value) + private void ValidateMinMax(decimal value) { if (value < Minimum) { diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs index e994ffdd15..9b467d682c 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs @@ -4,13 +4,13 @@ namespace Avalonia.Controls { public class NumericUpDownValueChangedEventArgs : RoutedEventArgs { - public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double oldValue, double newValue) : base(routedEvent) + public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, decimal oldValue, decimal newValue) : base(routedEvent) { OldValue = oldValue; NewValue = newValue; } - public double OldValue { get; } - public double NewValue { get; } + public decimal OldValue { get; } + public decimal NewValue { get; } } } From 6c0d9e86bf175f265e5c0a5813fa6887a4c8a5d5 Mon Sep 17 00:00:00 2001 From: Scott Whitney Date: Fri, 28 May 2021 12:07:41 +0930 Subject: [PATCH 008/184] ApiCompatBaseline --- src/Avalonia.Controls/ApiCompatBaseline.txt | 26 ++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index a79b3b4d7b..2f312bb266 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -4,6 +4,30 @@ 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.DirectProperty Avalonia.DirectProperty Avalonia.Controls.NumericUpDown.ValueProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.IncrementProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.MaximumProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.MinimumProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Increment.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Increment.set(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Maximum.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Maximum.set(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Minimum.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Minimum.set(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceIncrement(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceMaximum(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceMinimum(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceValue(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnIncrementChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnMaximumChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnMinimumChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnValueChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.RaiseValueChangedEvent(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Value.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Value.set(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChangedEventArgs..ctor(Avalonia.Interactivity.RoutedEvent, System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' 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 +35,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: 36 From 78f7726289cf8a002c36f33ac49153e33407528b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 May 2021 17:09:14 +0200 Subject: [PATCH 009/184] Don't activate window when extending client area. Fixes #5988 --- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 082aca1109..3a3342fd14 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -900,7 +900,7 @@ namespace Avalonia.Win32 IntPtr.Zero, rcWindow.left, rcWindow.top, rcClient.Width, rcClient.Height, - SetWindowPosFlags.SWP_FRAMECHANGED); + SetWindowPosFlags.SWP_FRAMECHANGED | SetWindowPosFlags.SWP_NOACTIVATE); if (_isClientAreaExtended && WindowState != WindowState.FullScreen) { From 7ffc527cd5f76237ae92d32d2a81be6153853759 Mon Sep 17 00:00:00 2001 From: Scott Whitney Date: Sat, 29 May 2021 07:56:11 +0930 Subject: [PATCH 010/184] Added Obsolete attribute to CultureInfo --- src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 659bc272e0..3a83d9ed7c 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -45,6 +45,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// + [Obsolete] public static readonly DirectProperty CultureInfoProperty = AvaloniaProperty.RegisterDirect(nameof(CultureInfo), o => o.CultureInfo, (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); @@ -186,6 +187,7 @@ namespace Avalonia.Controls /// /// Gets or sets the current CultureInfo. /// + [Obsolete("CultureInfo is obsolete, please use NumberFormat instead.")] public CultureInfo CultureInfo { get { return _cultureInfo; } From f2e93ecb87668f827f3ab03e91be44391fcc59fc Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 30 May 2021 00:59:05 +0200 Subject: [PATCH 011/184] Optimize shape rendering (pen creation mostly). --- src/Avalonia.Controls/Shapes/Shape.cs | 26 ++++- .../Media/Immutable/ImmutableDashStyle.cs | 12 +- .../Media/Immutable/ImmutablePen.cs | 2 +- .../Rendering/SceneGraph/SceneBuilder.cs | 2 + .../Rendering/SceneGraph/VisualNode.cs | 5 + .../NullDrawingContextImpl.cs | 103 ++++++++++++++++++ .../NullRenderingPlatform.cs | 4 +- .../Rendering/ShapeRendering.cs | 53 +++++++++ 8 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs create mode 100644 tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 0b7595ec9a..0d1d9e3ffe 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Collections; using Avalonia.Media; +using Avalonia.Media.Immutable; #nullable enable @@ -199,8 +200,29 @@ namespace Avalonia.Controls.Shapes if (geometry != null) { - var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray, StrokeDashOffset), - StrokeLineCap, StrokeJoin); + var stroke = Stroke; + + ImmutablePen? pen = null; + + if (stroke != null) + { + var strokeDashArray = StrokeDashArray; + + ImmutableDashStyle? dashStyle = null; + + if (strokeDashArray != null && strokeDashArray.Count > 0) + { + dashStyle = new ImmutableDashStyle(strokeDashArray, StrokeDashOffset); + } + + pen = new ImmutablePen( + stroke.ToImmutable(), + StrokeThickness, + dashStyle, + StrokeLineCap, + StrokeJoin); + } + context.DrawGeometry(Fill, pen, geometry); } } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs index e9a52fe6ed..2dd188e0a9 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs @@ -10,6 +10,8 @@ namespace Avalonia.Media.Immutable /// public class ImmutableDashStyle : IDashStyle, IEquatable { + private readonly double[] _dashes; + /// /// Initializes a new instance of the class. /// @@ -17,12 +19,12 @@ namespace Avalonia.Media.Immutable /// The dash sequence offset. public ImmutableDashStyle(IEnumerable dashes, double offset) { - Dashes = (IReadOnlyList)dashes?.ToList() ?? Array.Empty(); + _dashes = dashes?.ToArray() ?? Array.Empty(); Offset = offset; } /// - public IReadOnlyList Dashes { get; } + public IReadOnlyList Dashes => _dashes; /// public double Offset { get; } @@ -56,9 +58,9 @@ namespace Avalonia.Media.Immutable var hashCode = 717868523; hashCode = hashCode * -1521134295 + Offset.GetHashCode(); - if (Dashes != null) + if (_dashes != null) { - foreach (var i in Dashes) + foreach (var i in _dashes) { hashCode = hashCode * -1521134295 + i.GetHashCode(); } @@ -69,7 +71,7 @@ namespace Avalonia.Media.Immutable private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList right) { - if (left == right) + if (ReferenceEquals(left, right)) { return true; } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs index e586eaf3a9..32624fbf45 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs @@ -23,7 +23,7 @@ namespace Avalonia.Media.Immutable ImmutableDashStyle dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, - double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) + double miterLimit = 10.0) : this(new ImmutableSolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) { } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 56f05db04e..f2a09b815e 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -249,6 +249,8 @@ namespace Avalonia.Rendering.SceneGraph { var visualChildren = (IList) visual.VisualChildren; + node.TryPreallocateChildren(visualChildren.Count); + if (visualChildren.Count == 1) { var childNode = GetOrCreateChildNode(scene, visualChildren[0], node); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index d0439feed2..db6b606b41 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -358,6 +358,11 @@ namespace Avalonia.Rendering.SceneGraph internal void TryPreallocateChildren(int count) { + if (count == 0) + { + return; + } + EnsureChildrenCreated(count); } diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs new file mode 100644 index 0000000000..7626be7760 --- /dev/null +++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs @@ -0,0 +1,103 @@ +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Benchmarks +{ + internal class NullDrawingContextImpl : IDrawingContextImpl + { + public void Dispose() + { + } + + public Matrix Transform { get; set; } + + public void Clear(Color color) + { + } + + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + } + + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + } + + public void DrawLine(IPen pen, Point p1, Point p2) + { + } + + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) + { + } + + public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) + { + } + + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) + { + } + + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + return null; + } + + public void PushClip(Rect clip) + { + } + + public void PushClip(RoundedRect clip) + { + } + + public void PopClip() + { + } + + public void PushOpacity(double opacity) + { + } + + public void PopOpacity() + { + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + } + + public void PopOpacityMask() + { + } + + public void PushGeometryClip(IGeometryImpl clip) + { + } + + public void PopGeometryClip() + { + } + + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + } + + public void PopBitmapBlendMode() + { + } + + public void Custom(ICustomDrawOperation custom) + { + } + } +} diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 8792cb5cb1..876a0de643 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -18,12 +18,12 @@ namespace Avalonia.Benchmarks public IGeometryImpl CreateEllipseGeometry(Rect rect) { - throw new NotImplementedException(); + return new MockStreamGeometryImpl(); } public IGeometryImpl CreateLineGeometry(Point p1, Point p2) { - throw new NotImplementedException(); + return new MockStreamGeometryImpl(); } public IGeometryImpl CreateRectangleGeometry(Rect rect) diff --git a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs new file mode 100644 index 0000000000..b0db806afa --- /dev/null +++ b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs @@ -0,0 +1,53 @@ +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Platform; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Rendering +{ + [MemoryDiagnoser] + public class ShapeRendering + { + private readonly DrawingContext _drawingContext; + private readonly Line _lineFill; + private readonly Line _lineFillAndStroke; + private readonly Line _lineNoBrushes; + private readonly Line _lineStroke; + + public ShapeRendering() + { + _lineNoBrushes = new Line(); + _lineStroke = new Line { Stroke = new SolidColorBrush() }; + _lineFill = new Line { Fill = new SolidColorBrush() }; + _lineFillAndStroke = new Line { Stroke = new SolidColorBrush(), Fill = new SolidColorBrush() }; + + _drawingContext = new DrawingContext(new NullDrawingContextImpl(), true); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new NullRenderingPlatform()); + } + + [Benchmark] + public void Render_Line_NoBrushes() + { + _lineNoBrushes.Render(_drawingContext); + } + + [Benchmark] + public void Render_Line_WithStroke() + { + _lineStroke.Render(_drawingContext); + } + + [Benchmark] + public void Render_Line_WithFill() + { + _lineFill.Render(_drawingContext); + } + + [Benchmark] + public void Render_Line_WithFillAndStroke() + { + _lineFillAndStroke.Render(_drawingContext); + } + } +} From ae4e0741d6922f73b3eb89a16c878e5aea24ca1b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 May 2021 23:07:16 -0400 Subject: [PATCH 012/184] Add MemberNotNullAttribute and MemberNotNullWhenAttribute attributes --- .../Metadata/NullableAttributes.cs | 81 ++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Metadata/NullableAttributes.cs b/src/Avalonia.Base/Metadata/NullableAttributes.cs index 91f5e81863..b6f0f3a47c 100644 --- a/src/Avalonia.Base/Metadata/NullableAttributes.cs +++ b/src/Avalonia.Base/Metadata/NullableAttributes.cs @@ -1,6 +1,5 @@ #pragma warning disable MA0048 // File name must match type name #define INTERNAL_NULLABLE_ATTRIBUTES -#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 // https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs @@ -10,6 +9,7 @@ namespace System.Diagnostics.CodeAnalysis { +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 /// Specifies that null is allowed as an input even if the corresponding type disallows it. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] #if INTERNAL_NULLABLE_ATTRIBUTES @@ -136,5 +136,82 @@ namespace System.Diagnostics.CodeAnalysis /// Gets the condition parameter value. public bool ParameterValue { get; } } -} +#endif // NETSTANDARD2_0 attributes + +#if NETSTANDARD2_1 || NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + /// + /// Specifies that the method or property will ensure that the listed field and property members have + /// not- values. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public #endif + sealed class MemberNotNullAttribute : Attribute + { + /// Gets field or property member names. + public string[] Members { get; } + + /// Initializes the attribute with a field or property member. + /// The field or property member that is promised to be not-null. + public MemberNotNullAttribute(string member) + { + Members = new[] { member }; + } + + /// Initializes the attribute with the list of field and property members. + /// The list of field and property members that are promised to be not-null. + public MemberNotNullAttribute(params string[] members) + { + Members = members; + } + } + + /// + /// Specifies that the method or property will ensure that the listed field and property members have + /// non- values when returning with the specified return value condition. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MemberNotNullWhenAttribute : Attribute + { + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, + /// the associated parameter will not be . + /// + /// The field or property member that is promised to be not-. + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// + /// The return value condition. If the method returns this value, + /// the associated parameter will not be . + /// + /// The list of field and property members that are promised to be not-null. + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + } +#endif // NETSTANDARD2_1 attributes +} + From 522f6b97ae755be721538a6b0727c7388a481434 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 May 2021 23:07:40 -0400 Subject: [PATCH 013/184] Enable nullable on Avalonia.Diagnostics --- .../Avalonia.Diagnostics.csproj | 4 ++ .../Diagnostics/Controls/ThicknessEditor.cs | 8 ++-- .../Converters/BoolToOpacityConverter.cs | 11 +++-- .../Converters/EnumToCheckedConverter.cs | 4 +- .../Diagnostics/DevTools.cs | 4 +- .../Diagnostics/Models/ConsoleContext.cs | 4 +- .../Diagnostics/Models/EventChainLink.cs | 4 +- .../ViewModels/AvaloniaPropertyViewModel.cs | 42 +++++++------------ .../ViewModels/ClrPropertyViewModel.cs | 7 ++-- .../ViewModels/ConsoleViewModel.cs | 3 +- .../ViewModels/ControlDetailsViewModel.cs | 16 +++---- .../ViewModels/ControlLayoutViewModel.cs | 10 ++--- .../ViewModels/EventOwnerTreeNode.cs | 2 +- .../Diagnostics/ViewModels/EventTreeNode.cs | 19 ++++----- .../ViewModels/EventTreeNodeBase.cs | 6 +-- .../ViewModels/EventsPageViewModel.cs | 11 +++-- .../Diagnostics/ViewModels/FilterViewModel.cs | 17 ++++---- .../Diagnostics/ViewModels/FiredEvent.cs | 15 +++---- .../Diagnostics/ViewModels/LogicalTreeNode.cs | 8 ++-- .../Diagnostics/ViewModels/MainViewModel.cs | 19 +++++---- .../ViewModels/PropertyViewModel.cs | 2 +- .../ViewModels/ResourceSetterViewModel.cs | 2 +- .../Diagnostics/ViewModels/SetterViewModel.cs | 4 +- .../Diagnostics/ViewModels/TreeNode.cs | 16 +++---- .../ViewModels/TreeNodeCollection.cs | 29 ++++++++++--- .../ViewModels/TreePageViewModel.cs | 14 +++---- .../Diagnostics/ViewModels/ViewModelBase.cs | 10 ++--- .../Diagnostics/ViewModels/VisualTreeNode.cs | 8 ++-- .../Diagnostics/Views/ConsoleView.xaml.cs | 8 +++- .../Diagnostics/Views/MainView.xaml.cs | 6 ++- .../Diagnostics/Views/MainWindow.xaml.cs | 31 +++++++++----- .../Diagnostics/Views/TreePageView.xaml.cs | 14 ++++--- .../Diagnostics/VisualTreeDebug.cs | 2 +- 33 files changed, 198 insertions(+), 162 deletions(-) diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index db8684747d..35de491668 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -3,12 +3,16 @@ netstandard2.0 Avalonia Avalonia.Diagnostics + enable %(Filename) + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs index e5b3b080e2..cb98fb70f3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs @@ -10,8 +10,8 @@ namespace Avalonia.Diagnostics.Controls AvaloniaProperty.RegisterDirect(nameof(Thickness), o => o.Thickness, (o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay); - public static readonly DirectProperty HeaderProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, + public static readonly DirectProperty HeaderProperty = + AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, (o, v) => o.Header = v); public static readonly DirectProperty IsPresentProperty = @@ -36,7 +36,7 @@ namespace Avalonia.Diagnostics.Controls AvaloniaProperty.Register(nameof(Highlight)); private Thickness _thickness; - private string _header; + private string? _header; private bool _isPresent = true; private double _left; private double _top; @@ -50,7 +50,7 @@ namespace Avalonia.Diagnostics.Controls set => SetAndRaise(ThicknessProperty, ref _thickness, value); } - public string Header + public string? Header { get => _header; set => SetAndRaise(HeaderProperty, ref _header, value); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs index 63ac3ab62f..0b9044e65e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs @@ -8,12 +8,17 @@ namespace Avalonia.Diagnostics.Converters { public double Opacity { get; set; } - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - return (bool)value ? 1d : Opacity; + if (value is bool boolean && boolean) + { + return 1d; + } + + return Opacity; } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs index 8d10981ba7..4863782f44 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs @@ -7,12 +7,12 @@ namespace Avalonia.Diagnostics.Converters { internal class EnumToCheckedConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { return Equals(value, parameter); } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is bool isChecked && isChecked) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 7942d22962..78e808595f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -6,8 +6,6 @@ using Avalonia.Diagnostics.Views; using Avalonia.Input; using Avalonia.Interactivity; -#nullable enable - namespace Avalonia.Diagnostics { public static class DevTools @@ -74,7 +72,7 @@ namespace Avalonia.Diagnostics private static void DevToolsClosed(object sender, EventArgs e) { var window = (MainWindow)sender; - s_open.Remove(window.Root); + s_open.Remove(window.Root!); window.Closed -= DevToolsClosed; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs index 5927bd785e..4f4579c7d9 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs @@ -22,8 +22,8 @@ The following commands are available: clear(): Clear the output history "; - public dynamic e { get; internal set; } - public dynamic root { get; internal set; } + public dynamic? e { get; internal set; } + public dynamic? root { get; internal set; } internal static object NoOutput { get; } = new object(); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs index 36fe12d89c..4f493bdcc2 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs @@ -7,9 +7,7 @@ namespace Avalonia.Diagnostics.Models { public EventChainLink(object handler, bool handled, RoutingStrategies route) { - Contract.Requires(handler != null); - - Handler = handler; + Handler = handler ?? throw new ArgumentNullException(nameof(handler)); Handled = handled; Route = route; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index a9353eba8b..21265f2cfc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -1,5 +1,4 @@ -using System.ComponentModel; -using Avalonia.Collections; +using System.Diagnostics.CodeAnalysis; namespace Avalonia.Diagnostics.ViewModels { @@ -7,7 +6,7 @@ namespace Avalonia.Diagnostics.ViewModels { private readonly AvaloniaObject _target; private string _type; - private object _value; + private object? _value; private string _priority; private string _group; @@ -20,12 +19,6 @@ namespace Avalonia.Diagnostics.ViewModels $"[{property.OwnerType.Name}.{property.Name}]" : property.Name; - if (property.IsDirect) - { - _group = "Properties"; - Priority = "Direct"; - } - Update(); } @@ -34,11 +27,7 @@ namespace Avalonia.Diagnostics.ViewModels public override string Name { get; } public bool IsAttached => Property.IsAttached; - public string Priority - { - get => _priority; - private set => RaiseAndSetIfChanged(ref _priority, value); - } + public string Priority => _priority; public override string Type => _type; @@ -56,40 +45,37 @@ namespace Avalonia.Diagnostics.ViewModels } } - public override string Group - { - get => _group; - } + public override string Group => _group; + [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] public override void Update() { if (Property.IsDirect) { RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type)); + RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); + RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); + + _group = "Properties"; } else { var val = _target.GetDiagnostic(Property); RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type)); + RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); if (val != null) { - SetGroup(IsAttached ? "Attached Properties" : "Properties"); - Priority = val.Priority.ToString(); + RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority)); + RaiseAndSetIfChanged(ref _group, IsAttached ? "Attached Properties" : "Properties", nameof(Group)); } else { - SetGroup(Priority = "Unset"); + RaiseAndSetIfChanged(ref _priority, "Unset", nameof(Priority)); + RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group)); } } } - - private void SetGroup(string group) - { - RaiseAndSetIfChanged(ref _group, group, nameof(Group)); - } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index af5e254204..19e4a702eb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -1,4 +1,4 @@ -using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace Avalonia.Diagnostics.ViewModels @@ -7,7 +7,7 @@ namespace Avalonia.Diagnostics.ViewModels { private readonly object _target; private string _type; - private object _value; + private object? _value; public ClrPropertyViewModel(object o, PropertyInfo property) { @@ -47,11 +47,12 @@ namespace Avalonia.Diagnostics.ViewModels } } + [MemberNotNull(nameof(_type))] public override void Update() { var val = Property.GetValue(_target); RaiseAndSetIfChanged(ref _value, val, nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type)); + RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs index 0e0c44ded8..717b49d074 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs @@ -15,11 +15,12 @@ namespace Avalonia.Diagnostics.ViewModels private int _historyIndex = -1; private string _input; private bool _isVisible; - private ScriptState _state; + private ScriptState? _state; public ConsoleViewModel(Action updateContext) { _context = new ConsoleContext(this); + _input = string.Empty; _updateContext = updateContext; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index b1ff8ae98d..74709cb91a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -18,10 +18,10 @@ namespace Avalonia.Diagnostics.ViewModels { private readonly IVisual _control; private readonly IDictionary> _propertyIndex; - private AvaloniaPropertyViewModel _selectedProperty; + private AvaloniaPropertyViewModel? _selectedProperty; private bool _snapshotStyles; private bool _showInactiveStyles; - private string _styleStatus; + private string? _styleStatus; public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) { @@ -83,7 +83,8 @@ namespace Avalonia.Diagnostics.ViewModels { foreach (var setter in style.Setters) { - if (setter is Setter regularSetter) + if (setter is Setter regularSetter + && regularSetter.Property != null) { var setterValue = regularSetter.Value; @@ -115,13 +116,14 @@ namespace Avalonia.Diagnostics.ViewModels } } - private (object resourceKey, bool isDynamic)? GetResourceInfo(object value) + private (object resourceKey, bool isDynamic)? GetResourceInfo(object? value) { if (value is StaticResourceExtension staticResource) { return (staticResource.ResourceKey, false); } - else if (value is DynamicResourceExtension dynamicResource) + else if (value is DynamicResourceExtension dynamicResource + && dynamicResource.ResourceKey != null) { return (dynamicResource.ResourceKey, true); } @@ -137,7 +139,7 @@ namespace Avalonia.Diagnostics.ViewModels public ObservableCollection PseudoClasses { get; } - public AvaloniaPropertyViewModel SelectedProperty + public AvaloniaPropertyViewModel? SelectedProperty { get => _selectedProperty; set => RaiseAndSetIfChanged(ref _selectedProperty, value); @@ -155,7 +157,7 @@ namespace Avalonia.Diagnostics.ViewModels set => RaiseAndSetIfChanged(ref _showInactiveStyles, value); } - public string StyleStatus + public string? StyleStatus { get => _styleStatus; set => RaiseAndSetIfChanged(ref _styleStatus, value); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs index b0718bc6ce..f691698488 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs @@ -12,14 +12,14 @@ namespace Avalonia.Diagnostics.ViewModels private readonly IVisual _control; private Thickness _borderThickness; private double _height; - private string _heightConstraint; + private string? _heightConstraint; private HorizontalAlignment _horizontalAlignment; private Thickness _marginThickness; private Thickness _paddingThickness; private bool _updatingFromControl; private VerticalAlignment _verticalAlignment; private double _width; - private string _widthConstraint; + private string? _widthConstraint; public ControlLayoutViewModel(IVisual control) { @@ -80,13 +80,13 @@ namespace Avalonia.Diagnostics.ViewModels private set => RaiseAndSetIfChanged(ref _height, value); } - public string WidthConstraint + public string? WidthConstraint { get => _widthConstraint; private set => RaiseAndSetIfChanged(ref _widthConstraint, value); } - public string HeightConstraint + public string? HeightConstraint { get => _heightConstraint; private set => RaiseAndSetIfChanged(ref _heightConstraint, value); @@ -112,7 +112,7 @@ namespace Avalonia.Diagnostics.ViewModels { if (_control is IAvaloniaObject ao) { - string CreateConstraintInfo(StyledProperty minProperty, StyledProperty maxProperty) + string? CreateConstraintInfo(StyledProperty minProperty, StyledProperty maxProperty) { bool hasMin = ao.IsSet(minProperty); bool hasMax = ao.IsSet(maxProperty); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs index b56374d353..5b7ddc98ee 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs @@ -27,7 +27,7 @@ namespace Avalonia.Diagnostics.ViewModels if (_updateChildren && value != null) { - foreach (var child in Children) + foreach (var child in Children!) { try { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs index ea54302ebd..f6f57b769d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs @@ -11,16 +11,13 @@ namespace Avalonia.Diagnostics.ViewModels { private readonly EventsPageViewModel _parentViewModel; private bool _isRegistered; - private FiredEvent _currentEvent; + private FiredEvent? _currentEvent; public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm) : base(parent, @event.Name) { - Contract.Requires(@event != null); - Contract.Requires(vm != null); - - Event = @event; - _parentViewModel = vm; + Event = @event ?? throw new ArgumentNullException(nameof(@event)); + _parentViewModel = vm ?? throw new ArgumentNullException(nameof(vm)); } public RoutedEvent Event { get; } @@ -73,7 +70,7 @@ namespace Avalonia.Diagnostics.ViewModels var handled = e.Handled; var route = e.Route; - Action handler = delegate + void handler() { if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e)) { @@ -98,14 +95,16 @@ namespace Avalonia.Diagnostics.ViewModels private static bool BelongsToDevTool(IVisual v) { - while (v != null) + var current = v; + + while (current != null) { - if (v is MainView || v is MainWindow) + if (current is MainView || current is MainWindow) { return true; } - v = v.VisualParent; + current = current.VisualParent; } return false; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs index c27cad29e8..e6d7335297 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs @@ -10,14 +10,14 @@ namespace Avalonia.Diagnostics.ViewModels private bool? _isEnabled = false; private bool _isVisible; - protected EventTreeNodeBase(EventTreeNodeBase parent, string text) + protected EventTreeNodeBase(EventTreeNodeBase? parent, string text) { Parent = parent; Text = text; IsVisible = true; } - public IAvaloniaReadOnlyList Children + public IAvaloniaReadOnlyList? Children { get; protected set; @@ -41,7 +41,7 @@ namespace Avalonia.Diagnostics.ViewModels set => RaiseAndSetIfChanged(ref _isVisible, value); } - public EventTreeNodeBase Parent + public EventTreeNodeBase? Parent { get; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs index 7a157dec62..fbcedb2e74 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.ComponentModel; using System.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.Models; @@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels }; private readonly MainViewModel _mainViewModel; - private FiredEvent _selectedEvent; - private EventTreeNodeBase _selectedNode; + private FiredEvent? _selectedEvent; + private EventTreeNodeBase? _selectedNode; public EventsPageViewModel(MainViewModel mainViewModel) { @@ -48,13 +47,13 @@ namespace Avalonia.Diagnostics.ViewModels public ObservableCollection RecordedEvents { get; } = new ObservableCollection(); - public FiredEvent SelectedEvent + public FiredEvent? SelectedEvent { get => _selectedEvent; set => RaiseAndSetIfChanged(ref _selectedEvent, value); } - public EventTreeNodeBase SelectedNode + public EventTreeNodeBase? SelectedNode { get => _selectedNode; set => RaiseAndSetIfChanged(ref _selectedNode, value); @@ -99,7 +98,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - static EventTreeNodeBase FindNode(EventTreeNodeBase node, RoutedEvent eventType) + static EventTreeNodeBase? FindNode(EventTreeNodeBase node, RoutedEvent eventType) { if (node is EventTreeNode eventNode && eventNode.Event == eventType) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs index 0d472a5d8f..e592c89892 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs @@ -11,10 +11,13 @@ namespace Avalonia.Diagnostics.ViewModels private readonly Dictionary _errors = new Dictionary(); private string _filterString = string.Empty; private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter; - private string _processedFilter; - private Regex _filterRegex; + private Regex? _filterRegex; - public event EventHandler RefreshFilter; + public event EventHandler? RefreshFilter; + + public bool HasErrors => _errors.Count > 0; + + public event EventHandler? ErrorsChanged; public bool Filter(string input) { @@ -31,13 +34,11 @@ namespace Avalonia.Diagnostics.ViewModels } } - _processedFilter = FilterString.Trim(); - try { var options = RegexOptions.Compiled; var pattern = UseRegexFilter - ? _processedFilter : Regex.Escape(_processedFilter); + ? FilterString.Trim() : Regex.Escape(FilterString.Trim()); if (!UseCaseSensitiveFilter) { options |= RegexOptions.IgnoreCase; @@ -116,9 +117,5 @@ namespace Avalonia.Diagnostics.ViewModels yield return error; } } - - public bool HasErrors => _errors.Count > 0; - - public event EventHandler ErrorsChanged; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs index 5fb528eead..32df2f8745 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs @@ -8,15 +8,12 @@ namespace Avalonia.Diagnostics.ViewModels internal class FiredEvent : ViewModelBase { private readonly RoutedEventArgs _eventArgs; - private EventChainLink _handledBy; + private EventChainLink? _handledBy; public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator) { - Contract.Requires(eventArgs != null); - Contract.Requires(originator != null); - - _eventArgs = eventArgs; - Originator = originator; + _eventArgs = eventArgs ?? throw new ArgumentNullException(nameof(eventArgs)); + Originator = originator ?? throw new ArgumentNullException(nameof(originator)); AddToChain(originator); } @@ -25,7 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels return e == _eventArgs; } - public RoutedEvent Event => _eventArgs.RoutedEvent; + public RoutedEvent Event => _eventArgs.RoutedEvent!; public bool IsHandled => HandledBy?.Handled == true; @@ -38,7 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels if (IsHandled) { return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine + - $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; + $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy!.HandlerName}"; } return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}"; @@ -47,7 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels public EventChainLink Originator { get; } - public EventChainLink HandledBy + public EventChainLink? HandledBy { get => _handledBy; set diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs index 38788ef8ee..04215fa8ae 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs @@ -7,22 +7,24 @@ namespace Avalonia.Diagnostics.ViewModels { internal class LogicalTreeNode : TreeNode { - public LogicalTreeNode(ILogical logical, TreeNode parent) + public LogicalTreeNode(ILogical logical, TreeNode? parent) : base((Control)logical, parent) { Children = new LogicalTreeNodeCollection(this, logical); } + public override TreeNodeCollection Children { get; } + public static LogicalTreeNode[] Create(object control) { var logical = control as ILogical; - return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; + return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty(); } internal class LogicalTreeNodeCollection : TreeNodeCollection { private readonly ILogical _control; - private IDisposable _subscription; + private IDisposable? _subscription; public LogicalTreeNodeCollection(TreeNode owner, ILogical control) : base(owner) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 49263eafdc..4a945c253f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,10 +1,11 @@ using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Threading; -using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { @@ -17,8 +18,8 @@ namespace Avalonia.Diagnostics.ViewModels private readonly IDisposable _pointerOverSubscription; private ViewModelBase _content; private int _selectedTab; - private string _focusedControl; - private string _pointerOverElement; + private string? _focusedControl; + private string? _pointerOverElement; private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeDirtyRects; private bool _showFpsOverlay; @@ -84,6 +85,7 @@ namespace Avalonia.Diagnostics.ViewModels public ViewModelBase Content { get { return _content; } + [MemberNotNull(nameof(_content))] private set { if (_content is TreePageViewModel oldTree && @@ -114,34 +116,35 @@ namespace Avalonia.Diagnostics.ViewModels public int SelectedTab { get { return _selectedTab; } + [MemberNotNull(nameof(_content))] set { _selectedTab = value; switch (value) { - case 0: - Content = _logicalTree; - break; case 1: Content = _visualTree; break; case 2: Content = _events; break; + default: + Content = _logicalTree; + break; } RaisePropertyChanged(); } } - public string FocusedControl + public string? FocusedControl { get { return _focusedControl; } private set { RaiseAndSetIfChanged(ref _focusedControl, value); } } - public string PointerOverElement + public string? PointerOverElement { get { return _pointerOverElement; } private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index e23d6f1471..779d74d554 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -18,7 +18,7 @@ namespace Avalonia.Diagnostics.ViewModels public abstract string Value { get; set; } public abstract void Update(); - protected static string ConvertToString(object value) + protected static string ConvertToString(object? value) { if (value is null) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs index a82e13fcfa..2655b0e31f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs @@ -8,7 +8,7 @@ namespace Avalonia.Diagnostics.ViewModels public IBrush Tint { get; } - public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object resourceValue, bool isDynamic) : base(property, resourceValue) + public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object? resourceValue, bool isDynamic) : base(property, resourceValue) { Key = resourceKey; Tint = isDynamic ? Brushes.Orange : Brushes.Brown; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs index e835f5a878..1f0e93d751 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs @@ -11,7 +11,7 @@ namespace Avalonia.Diagnostics.ViewModels public string Name { get; } - public object Value { get; } + public object? Value { get; } public bool IsActive { @@ -25,7 +25,7 @@ namespace Avalonia.Diagnostics.ViewModels set => RaiseAndSetIfChanged(ref _isVisible, value); } - public SetterViewModel(AvaloniaProperty property, object value) + public SetterViewModel(AvaloniaProperty property, object? value) { Property = property; Name = property.Name; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 9363c28705..4cb470eeac 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -9,17 +9,18 @@ using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreeNode : ViewModelBase, IDisposable + internal abstract class TreeNode : ViewModelBase, IDisposable { - private IDisposable _classesSubscription; + private IDisposable? _classesSubscription; private string _classes; private bool _isExpanded; - public TreeNode(IVisual visual, TreeNode parent) + public TreeNode(IVisual visual, TreeNode? parent) { Parent = parent; Type = visual.GetType().Name; Visual = visual; + _classes = string.Empty; if (visual is IControl control) { @@ -51,10 +52,9 @@ namespace Avalonia.Diagnostics.ViewModels } } - public TreeNodeCollection Children + public abstract TreeNodeCollection Children { get; - protected set; } public string Classes @@ -63,7 +63,7 @@ namespace Avalonia.Diagnostics.ViewModels private set { RaiseAndSetIfChanged(ref _classes, value); } } - public string ElementName + public string? ElementName { get; } @@ -79,7 +79,7 @@ namespace Avalonia.Diagnostics.ViewModels set { RaiseAndSetIfChanged(ref _isExpanded, value); } } - public TreeNode Parent + public TreeNode? Parent { get; } @@ -92,7 +92,7 @@ namespace Avalonia.Diagnostics.ViewModels public void Dispose() { - _classesSubscription.Dispose(); + _classesSubscription?.Dispose(); Children.Dispose(); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs index 8b4f03bd23..e01258f4cc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs @@ -3,13 +3,15 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + using Avalonia.Collections; namespace Avalonia.Diagnostics.ViewModels { internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList, IDisposable { - private AvaloniaList _inner; + private AvaloniaList? _inner; public TreeNodeCollection(TreeNode owner) => Owner = owner; @@ -35,14 +37,30 @@ namespace Avalonia.Diagnostics.ViewModels public event NotifyCollectionChangedEventHandler CollectionChanged { - add => _inner.CollectionChanged += value; - remove => _inner.CollectionChanged -= value; + add + { + EnsureInitialized(); + _inner.CollectionChanged += value; + } + remove + { + EnsureInitialized(); + _inner.CollectionChanged -= value; + } } public event PropertyChangedEventHandler PropertyChanged { - add => _inner.PropertyChanged += value; - remove => _inner.PropertyChanged -= value; + add + { + EnsureInitialized(); + _inner.PropertyChanged += value; + } + remove + { + EnsureInitialized(); + _inner.PropertyChanged -= value; + } } public virtual void Dispose() @@ -66,6 +84,7 @@ namespace Avalonia.Diagnostics.ViewModels protected abstract void Initialize(AvaloniaList nodes); + [MemberNotNull(nameof(_inner))] private void EnsureInitialized() { if (_inner is null) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 85a7cb69a3..4b18cf414a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -6,8 +6,8 @@ namespace Avalonia.Diagnostics.ViewModels { internal class TreePageViewModel : ViewModelBase, IDisposable { - private TreeNode _selectedNode; - private ControlDetailsViewModel _details; + private TreeNode? _selectedNode; + private ControlDetailsViewModel? _details; public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes) { @@ -29,7 +29,7 @@ namespace Avalonia.Diagnostics.ViewModels public TreeNode[] Nodes { get; protected set; } - public TreeNode SelectedNode + public TreeNode? SelectedNode { get => _selectedNode; private set @@ -44,7 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public ControlDetailsViewModel Details + public ControlDetailsViewModel? Details { get => _details; private set @@ -68,7 +68,7 @@ namespace Avalonia.Diagnostics.ViewModels _details?.Dispose(); } - public TreeNode FindNode(IControl control) + public TreeNode? FindNode(IControl control) { foreach (var node in Nodes) { @@ -104,7 +104,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - private void ExpandNode(TreeNode node) + private void ExpandNode(TreeNode? node) { if (node != null) { @@ -113,7 +113,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - private TreeNode FindNode(TreeNode node, IControl control) + private TreeNode? FindNode(TreeNode node, IControl control) { if (node.Visual == control) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs index 66e9c34657..0bc44d5778 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs @@ -1,13 +1,13 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Avalonia.Diagnostics.ViewModels { internal class ViewModelBase : INotifyPropertyChanged { - private PropertyChangedEventHandler _propertyChanged; + private PropertyChangedEventHandler? _propertyChanged; private List events = new List(); public event PropertyChangedEventHandler PropertyChanged @@ -20,7 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels { } - protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null) + protected bool RaiseAndSetIfChanged([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string propertyName = null!) { if (!EqualityComparer.Default.Equals(field, value)) { @@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels return false; } - protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) + protected void RaisePropertyChanged([CallerMemberName] string propertyName = null!) { var e = new PropertyChangedEventArgs(propertyName); OnPropertyChanged(e); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index bc40edf477..48fa636664 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Diagnostics.ViewModels { internal class VisualTreeNode : TreeNode { - public VisualTreeNode(IVisual visual, TreeNode parent) + public VisualTreeNode(IVisual visual, TreeNode? parent) : base(visual, parent) { Children = new VisualTreeNodeCollection(this, visual); @@ -20,16 +20,18 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsInTemplate { get; private set; } + public override TreeNodeCollection Children { get; } + public static VisualTreeNode[] Create(object control) { var visual = control as IVisual; - return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; + return visual != null ? new[] { new VisualTreeNode(visual, null) } : Array.Empty(); } internal class VisualTreeNodeCollection : TreeNodeCollection { private readonly IVisual _control; - private IDisposable _subscription; + private IDisposable? _subscription; public VisualTreeNodeCollection(TreeNode owner, IVisual control) : base(owner) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs index ae70b59fde..a018e4180e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs @@ -40,12 +40,16 @@ namespace Avalonia.Diagnostics.Views private void InputKeyDown(object sender, KeyEventArgs e) { - var vm = (ConsoleViewModel)DataContext; + var vm = (ConsoleViewModel?)DataContext; + if (vm is null) + { + return; + } switch (e.Key) { case Key.Enter: - vm.Execute(); + _ = vm.Execute(); e.Handled = true; break; case Key.Up: diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs index 783709e54b..f30973fea9 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs @@ -27,7 +27,11 @@ namespace Avalonia.Diagnostics.Views public void ToggleConsole() { - var vm = (MainViewModel)DataContext; + var vm = (MainViewModel?)DataContext; + if (vm is null) + { + return; + } if (_consoleHeight == -1) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 330121321a..fdadf114c9 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -15,7 +15,7 @@ namespace Avalonia.Diagnostics.Views internal class MainWindow : Window, IStyleHost { private readonly IDisposable _keySubscription; - private TopLevel _root; + private TopLevel? _root; public MainWindow() { @@ -26,7 +26,7 @@ namespace Avalonia.Diagnostics.Views .Subscribe(RawKeyDown); } - public TopLevel Root + public TopLevel? Root { get => _root; set @@ -43,7 +43,7 @@ namespace Avalonia.Diagnostics.Views if (_root != null) { _root.Closed += RootClosed; - DataContext = new MainViewModel(value); + DataContext = new MainViewModel(_root); } else { @@ -53,15 +53,20 @@ namespace Avalonia.Diagnostics.Views } } - IStyleHost IStyleHost.StylingParent => null; + IStyleHost? IStyleHost.StylingParent => null; protected override void OnClosed(EventArgs e) { base.OnClosed(e); _keySubscription.Dispose(); - _root.Closed -= RootClosed; - _root = null; - ((MainViewModel)DataContext)?.Dispose(); + + if (_root != null) + { + _root.Closed -= RootClosed; + _root = null; + } + + ((MainViewModel?)DataContext)?.Dispose(); } private void InitializeComponent() @@ -71,12 +76,20 @@ namespace Avalonia.Diagnostics.Views private void RawKeyDown(RawKeyEventArgs e) { + var vm = (MainViewModel?)DataContext; + if (vm is null) + { + return; + } + const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift; if (e.Modifiers == modifiers) { +#pragma warning disable CS0618 // Type or member is obsolete var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default; - +#pragma warning restore CS0618 // Type or member is obsolete + var control = Root.GetVisualsAt(point, x => { if (x is AdornerLayer || !x.IsVisible) return false; @@ -87,7 +100,6 @@ namespace Avalonia.Diagnostics.Views if (control != null) { - var vm = (MainViewModel)DataContext; vm.SelectControl((IControl)control); } } @@ -97,7 +109,6 @@ namespace Avalonia.Diagnostics.Views { var enable = e.Key == Key.S; - var vm = (MainViewModel)DataContext; vm.EnableSnapshotStyles(enable); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 3e1a238b36..6ad6533e7a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -14,12 +14,13 @@ namespace Avalonia.Diagnostics.Views internal class TreePageView : UserControl { private readonly Panel _adorner; - private AdornerLayer _currentLayer; + private AdornerLayer? _currentLayer; private TreeView _tree; public TreePageView() { InitializeComponent(); + _tree = this.FindControl("tree"); _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized; _adorner = new Panel @@ -39,7 +40,13 @@ namespace Avalonia.Diagnostics.Views protected void AddAdorner(object sender, PointerEventArgs e) { - var node = (TreeNode)((Control)sender).DataContext; + var node = (TreeNode?)((Control)sender).DataContext; + var vm = (TreePageViewModel?)DataContext; + if (node is null || vm is null) + { + return; + } + var visual = (Visual)node.Visual; _currentLayer = AdornerLayer.GetAdornerLayer(visual); @@ -53,8 +60,6 @@ namespace Avalonia.Diagnostics.Views _currentLayer.Children.Add(_adorner); AdornerLayer.SetAdornedElement(_adorner, visual); - var vm = (TreePageViewModel) DataContext; - if (vm.MainView.ShouldVisualizeMarginPadding) { var paddingBorder = (Border)_adorner.Children[0]; @@ -90,7 +95,6 @@ namespace Avalonia.Diagnostics.Views private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - _tree = this.FindControl("tree"); } private void TreeViewItemMaterialized(object sender, ItemContainerEventArgs e) diff --git a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs index 6f699339e7..4adcd32302 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs @@ -17,7 +17,7 @@ namespace Avalonia.Diagnostics private static void PrintVisualTree(IVisual visual, StringBuilder builder, int indent) { - Control control = visual as Control; + Control? control = visual as Control; builder.Append(Indent(indent - 1)); From de0be9326d924ac4675da0880fc2dafd5866bbdf Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 May 2021 23:30:47 -0400 Subject: [PATCH 014/184] Fix .NET 5 warnings --- .../Diagnostics/DevTools.cs | 6 +++--- .../Diagnostics/ViewLocator.cs | 4 ++-- .../ViewModels/ClrPropertyViewModel.cs | 2 +- .../ViewModels/ControlDetailsViewModel.cs | 19 ++++++++++--------- .../ViewModels/ControlLayoutViewModel.cs | 2 +- .../Diagnostics/ViewModels/EventTreeNode.cs | 4 ++-- .../Diagnostics/ViewModels/FilterViewModel.cs | 5 +++-- .../Diagnostics/ViewModels/MainViewModel.cs | 2 +- .../ViewModels/PropertyViewModel.cs | 6 +++--- .../ViewModels/ResourceSetterViewModel.cs | 6 ++++-- .../Diagnostics/ViewModels/SetterViewModel.cs | 6 ++++-- .../ViewModels/TreeNodeCollection.cs | 4 ++-- .../Diagnostics/ViewModels/ViewModelBase.cs | 2 +- .../Diagnostics/Views/ConsoleView.xaml.cs | 6 +++--- .../Diagnostics/Views/EventsPageView.xaml.cs | 2 +- .../Diagnostics/Views/MainView.xaml.cs | 2 +- .../Diagnostics/Views/MainWindow.xaml.cs | 2 +- .../Diagnostics/Views/TreePageView.xaml.cs | 12 ++++++------ 18 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 78e808595f..0e36c8f9cb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -22,7 +22,7 @@ namespace Avalonia.Diagnostics public static IDisposable Attach(TopLevel root, DevToolsOptions options) { - void PreviewKeyDown(object sender, KeyEventArgs e) + void PreviewKeyDown(object? sender, KeyEventArgs e) { if (options.Gesture.Matches(e)) { @@ -69,9 +69,9 @@ namespace Avalonia.Diagnostics return Disposable.Create(() => window?.Close()); } - private static void DevToolsClosed(object sender, EventArgs e) + private static void DevToolsClosed(object? sender, EventArgs e) { - var window = (MainWindow)sender; + var window = (MainWindow)sender!; s_open.Remove(window.Root!); window.Closed -= DevToolsClosed; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs index be3564e781..16852001da 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs @@ -9,12 +9,12 @@ namespace Avalonia.Diagnostics { public IControl Build(object data) { - var name = data.GetType().FullName.Replace("ViewModel", "View"); + var name = data.GetType().FullName!.Replace("ViewModel", "View"); var type = Type.GetType(name); if (type != null) { - return (Control)Activator.CreateInstance(type); + return (Control)Activator.CreateInstance(type)!; } else { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index 19e4a702eb..ab088a208c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -14,7 +14,7 @@ namespace Avalonia.Diagnostics.ViewModels _target = o; Property = property; - if (!property.DeclaringType.IsInterface) + if (property.DeclaringType == null || !property.DeclaringType.IsInterface) { Name = property.Name; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 74709cb91a..1f7205eb16 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -250,7 +250,7 @@ namespace Avalonia.Diagnostics.ViewModels .Select(x => new ClrPropertyViewModel(o, x)); } - private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (_propertyIndex.TryGetValue(e.Property, out var properties)) { @@ -263,9 +263,10 @@ namespace Avalonia.Diagnostics.ViewModels Layout.ControlPropertyChanged(sender, e); } - private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e) + private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e) { - if (_propertyIndex.TryGetValue(e.PropertyName, out var properties)) + if (e.PropertyName != null + && _propertyIndex.TryGetValue(e.PropertyName, out var properties)) { foreach (var property in properties) { @@ -279,7 +280,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e) + private void OnClassesChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (!SnapshotStyles) { @@ -351,10 +352,10 @@ namespace Avalonia.Diagnostics.ViewModels { public static PropertyComparer Instance { get; } = new PropertyComparer(); - public int Compare(PropertyViewModel x, PropertyViewModel y) + public int Compare(PropertyViewModel? x, PropertyViewModel? y) { - var groupX = GroupIndex(x.Group); - var groupY = GroupIndex(y.Group); + var groupX = GroupIndex(x?.Group); + var groupY = GroupIndex(y?.Group); if (groupX != groupY) { @@ -362,11 +363,11 @@ namespace Avalonia.Diagnostics.ViewModels } else { - return string.CompareOrdinal(x.Name, y.Name); + return string.CompareOrdinal(x?.Name, y?.Name); } } - private int GroupIndex(string group) + private int GroupIndex(string? group) { switch (group) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs index f691698488..4dc0c34c0a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs @@ -179,7 +179,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + public void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { try { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs index f6f57b769d..65fd81cc78 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs @@ -59,14 +59,14 @@ namespace Avalonia.Diagnostics.ViewModels } } - private void HandleEvent(object sender, RoutedEventArgs e) + private void HandleEvent(object? sender, RoutedEventArgs e) { if (!_isRegistered || IsEnabled == false) return; if (sender is IVisual v && BelongsToDevTool(v)) return; - var s = sender; + var s = sender!; var handled = e.Handled; var route = e.Route; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs index e592c89892..5b27236f2e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs @@ -110,9 +110,10 @@ namespace Avalonia.Diagnostics.ViewModels } } - public IEnumerable GetErrors(string propertyName) + public IEnumerable GetErrors(string? propertyName) { - if (_errors.TryGetValue(propertyName, out var error)) + if (propertyName != null + && _errors.TryGetValue(propertyName, out var error)) { yield return error; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 4a945c253f..98afafe7d4 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -190,7 +190,7 @@ namespace Avalonia.Diagnostics.ViewModels FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; } - private void KeyboardPropertyChanged(object sender, PropertyChangedEventArgs e) + private void KeyboardPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index 779d74d554..bfd098985a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -31,13 +31,13 @@ namespace Avalonia.Diagnostics.ViewModels if (!converter.CanConvertTo(typeof(string)) || converter.GetType() == typeof(CollectionConverter)) { - return value.ToString(); + return value.ToString() ?? "(null)"; } return converter.ConvertToString(value); } - private static object InvokeParse(string s, Type targetType) + private static object? InvokeParse(string s, Type targetType) { var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); @@ -56,7 +56,7 @@ namespace Avalonia.Diagnostics.ViewModels throw new InvalidCastException("Unable to convert value."); } - protected static object ConvertFromString(string s, Type targetType) + protected static object? ConvertFromString(string s, Type targetType) { var converter = TypeDescriptor.GetConverter(targetType); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs index 2655b0e31f..e93dc7361b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs @@ -16,12 +16,14 @@ namespace Avalonia.Diagnostics.ViewModels public void CopyResourceKey() { - if (Key is null) + var textToCopy = Key?.ToString(); + + if (textToCopy is null) { return; } - CopyToClipboard(Key.ToString()); + CopyToClipboard(textToCopy); } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs index 1f0e93d751..38cbefcb93 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs @@ -36,12 +36,14 @@ namespace Avalonia.Diagnostics.ViewModels public void CopyValue() { - if (Value is null) + var textToCopy = Value?.ToString(); + + if (textToCopy is null) { return; } - CopyToClipboard(Value.ToString()); + CopyToClipboard(textToCopy); } public void CopyPropertyName() diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs index e01258f4cc..06a6fa9a74 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs @@ -35,7 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels protected TreeNode Owner { get; } - public event NotifyCollectionChangedEventHandler CollectionChanged + public event NotifyCollectionChangedEventHandler? CollectionChanged { add { @@ -49,7 +49,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public event PropertyChangedEventHandler PropertyChanged + public event PropertyChangedEventHandler? PropertyChanged { add { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs index 0bc44d5778..a2ee37c625 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs @@ -10,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels private PropertyChangedEventHandler? _propertyChanged; private List events = new List(); - public event PropertyChangedEventHandler PropertyChanged + public event PropertyChangedEventHandler? PropertyChanged { add { _propertyChanged += value; events.Add("added"); } remove { _propertyChanged -= value; events.Add("removed"); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs index a018e4180e..ab523fb75a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs @@ -30,15 +30,15 @@ namespace Avalonia.Diagnostics.Views AvaloniaXamlLoader.Load(this); } - private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e) + private void HistoryChanged(object? sender, NotifyCollectionChangedEventArgs e) { - if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control) + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is IControl control) { DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero); } } - private void InputKeyDown(object sender, KeyEventArgs e) + private void InputKeyDown(object? sender, KeyEventArgs e) { var vm = (ConsoleViewModel?)DataContext; if (vm is null) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs index 687a20c5f6..ba7ab41e35 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs @@ -53,7 +53,7 @@ namespace Avalonia.Diagnostics.Views } } - private void OnRecordedEventsChanged(object sender, NotifyCollectionChangedEventArgs e) + private void OnRecordedEventsChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (sender is ObservableCollection events) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs index f30973fea9..b688ad7676 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs @@ -58,7 +58,7 @@ namespace Avalonia.Diagnostics.Views AvaloniaXamlLoader.Load(this); } - private void PreviewKeyDown(object sender, KeyEventArgs e) + private void PreviewKeyDown(object? sender, KeyEventArgs e) { if (e.Key == Key.Escape) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index fdadf114c9..bbb8e76551 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -114,6 +114,6 @@ namespace Avalonia.Diagnostics.Views } } - private void RootClosed(object sender, EventArgs e) => Close(); + private void RootClosed(object? sender, EventArgs e) => Close(); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 6ad6533e7a..3543b1adea 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -38,9 +38,9 @@ namespace Avalonia.Diagnostics.Views }; } - protected void AddAdorner(object sender, PointerEventArgs e) + protected void AddAdorner(object? sender, PointerEventArgs e) { - var node = (TreeNode?)((Control)sender).DataContext; + var node = (TreeNode?)((Control)sender!).DataContext; var vm = (TreePageViewModel?)DataContext; if (node is null || vm is null) { @@ -79,7 +79,7 @@ namespace Avalonia.Diagnostics.Views return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom); } - protected void RemoveAdorner(object sender, PointerEventArgs e) + protected void RemoveAdorner(object? sender, PointerEventArgs e) { foreach (var border in _adorner.Children.OfType()) { @@ -97,15 +97,15 @@ namespace Avalonia.Diagnostics.Views AvaloniaXamlLoader.Load(this); } - private void TreeViewItemMaterialized(object sender, ItemContainerEventArgs e) + private void TreeViewItemMaterialized(object? sender, ItemContainerEventArgs e) { var item = (TreeViewItem)e.Containers[0].ContainerControl; item.TemplateApplied += TreeViewItemTemplateApplied; } - private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e) + private void TreeViewItemTemplateApplied(object? sender, TemplateAppliedEventArgs e) { - var item = (TreeViewItem)sender; + var item = (TreeViewItem)sender!; // This depends on the default tree item template. // We want to handle events in the item header but exclude events coming from children. From 23b4168cbc5553a4c7b63c73fd5202546a497c59 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 29 May 2021 23:57:16 -0400 Subject: [PATCH 015/184] Temporary do not use MemberNotNull --- .../ViewModels/AvaloniaPropertyViewModel.cs | 7 +-- .../ViewModels/ClrPropertyViewModel.cs | 8 +-- .../Diagnostics/ViewModels/MainViewModel.cs | 8 +-- .../ViewModels/TreeNodeCollection.cs | 50 ++++--------------- 4 files changed, 23 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 21265f2cfc..63f68501a7 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - namespace Avalonia.Diagnostics.ViewModels { internal class AvaloniaPropertyViewModel : PropertyViewModel @@ -10,7 +8,10 @@ namespace Avalonia.Diagnostics.ViewModels private string _priority; private string _group; +#nullable disable + // Remove "nullable disable" after MemberNotNull will work on our CI. public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property) +#nullable restore { _target = o; Property = property; @@ -47,7 +48,7 @@ namespace Avalonia.Diagnostics.ViewModels public override string Group => _group; - [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] + // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] public override void Update() { if (Property.IsDirect) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index ab088a208c..6b2dbb7bae 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; +using System.Reflection; namespace Avalonia.Diagnostics.ViewModels { @@ -9,7 +8,10 @@ namespace Avalonia.Diagnostics.ViewModels private string _type; private object? _value; +#nullable disable + // Remove "nullable disable" after MemberNotNull will work on our CI. public ClrPropertyViewModel(object o, PropertyInfo property) +#nullable restore { _target = o; Property = property; @@ -47,7 +49,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - [MemberNotNull(nameof(_type))] + // [MemberNotNull(nameof(_type))] public override void Update() { var val = Property.GetValue(_target); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 98afafe7d4..72491bebc2 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Diagnostics.Models; @@ -24,7 +23,10 @@ namespace Avalonia.Diagnostics.ViewModels private bool _shouldVisualizeDirtyRects; private bool _showFpsOverlay; +#nullable disable + // Remove "nullable disable" after MemberNotNull will work on our CI. public MainViewModel(TopLevel root) +#nullable restore { _root = root; _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); @@ -85,7 +87,7 @@ namespace Avalonia.Diagnostics.ViewModels public ViewModelBase Content { get { return _content; } - [MemberNotNull(nameof(_content))] + // [MemberNotNull(nameof(_content))] private set { if (_content is TreePageViewModel oldTree && @@ -116,7 +118,7 @@ namespace Avalonia.Diagnostics.ViewModels public int SelectedTab { get { return _selectedTab; } - [MemberNotNull(nameof(_content))] + // [MemberNotNull(nameof(_content))] set { _selectedTab = value; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs index 06a6fa9a74..c007411f49 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using Avalonia.Collections; @@ -15,52 +14,22 @@ namespace Avalonia.Diagnostics.ViewModels public TreeNodeCollection(TreeNode owner) => Owner = owner; - public TreeNode this[int index] - { - get - { - EnsureInitialized(); - return _inner[index]; - } - } + public TreeNode this[int index] => EnsureInitialized()[index]; - public int Count - { - get - { - EnsureInitialized(); - return _inner.Count; - } - } + public int Count => EnsureInitialized().Count; protected TreeNode Owner { get; } public event NotifyCollectionChangedEventHandler? CollectionChanged { - add - { - EnsureInitialized(); - _inner.CollectionChanged += value; - } - remove - { - EnsureInitialized(); - _inner.CollectionChanged -= value; - } + add => EnsureInitialized().CollectionChanged += value; + remove => EnsureInitialized().CollectionChanged -= value; } public event PropertyChangedEventHandler? PropertyChanged { - add - { - EnsureInitialized(); - _inner.PropertyChanged += value; - } - remove - { - EnsureInitialized(); - _inner.PropertyChanged -= value; - } + add => EnsureInitialized().PropertyChanged += value; + remove => EnsureInitialized().PropertyChanged -= value; } public virtual void Dispose() @@ -76,22 +45,21 @@ namespace Avalonia.Diagnostics.ViewModels public IEnumerator GetEnumerator() { - EnsureInitialized(); - return _inner.GetEnumerator(); + return EnsureInitialized().GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); protected abstract void Initialize(AvaloniaList nodes); - [MemberNotNull(nameof(_inner))] - private void EnsureInitialized() + private AvaloniaList EnsureInitialized() { if (_inner is null) { _inner = new AvaloniaList(); Initialize(_inner); } + return _inner; } } } From 86b9a60a4c2c037b32674a65cd01796cfab7727f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 31 May 2021 09:41:49 -0700 Subject: [PATCH 016/184] Fix custom hit testing (#5968) * Fix custom hit testing * Use ICustomHitTest as it's intended to be used in defered renderer Co-authored-by: Dan Walmsley Co-authored-by: Steven Kirk Co-authored-by: Steven Kirk --- src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs | 3 +-- src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs index 1d655bb691..74d804f2bf 100644 --- a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs @@ -52,8 +52,7 @@ namespace Avalonia.Controls.Primitives { if (InputPassThroughElement is object) { - var p = point.Transform(this.TransformToVisual(VisualRoot)!.Value); - var hit = VisualRoot.GetVisualAt(p, x => x != this); + var hit = VisualRoot.GetVisualAt(point, x => x != this); if (hit is object) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index 36aa08c2f9..d8e5baac97 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -257,7 +257,7 @@ namespace Avalonia.Rendering.SceneGraph if (childCount == 0 || wasVisited) { if ((wasVisited || FilterAndClip(node, ref clip)) && - (node.Visual is ICustomSimpleHitTest custom ? custom.HitTest(_point) : node.HitTest(_point))) + (node.Visual is ICustomHitTest custom ? custom.HitTest(_point) : node.HitTest(_point))) { _current = node.Visual; From 7d608a33eec1ef871345dbad579e031306676964 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 4 Jun 2021 10:28:44 +0200 Subject: [PATCH 017/184] Implemented ImageDrawing. --- src/Avalonia.Visuals/Media/ImageDrawing.cs | 53 +++++++++++ .../Media/ImageDrawingTests.cs | 84 ++++++++++++++++++ .../ImageDrawing_BottomRight.expected.png | Bin 0 -> 3526 bytes .../ImageDrawing_Fill.expected.png | Bin 0 -> 11529 bytes .../Media/ImageDrawing/github_icon.png | Bin 0 -> 17661 bytes .../ImageDrawing_BottomRight.expected.png | Bin 0 -> 3183 bytes .../ImageDrawing_Fill.expected.png | Bin 0 -> 6898 bytes .../Skia/Media/ImageDrawing/github_icon.png | Bin 0 -> 17661 bytes 8 files changed, 137 insertions(+) create mode 100644 src/Avalonia.Visuals/Media/ImageDrawing.cs create mode 100644 tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs create mode 100644 tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_Fill.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/ImageDrawing/github_icon.png create mode 100644 tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png create mode 100644 tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_Fill.expected.png create mode 100644 tests/TestFiles/Skia/Media/ImageDrawing/github_icon.png diff --git a/src/Avalonia.Visuals/Media/ImageDrawing.cs b/src/Avalonia.Visuals/Media/ImageDrawing.cs new file mode 100644 index 0000000000..82f97b52b4 --- /dev/null +++ b/src/Avalonia.Visuals/Media/ImageDrawing.cs @@ -0,0 +1,53 @@ +#nullable enable + +namespace Avalonia.Media +{ + /// + /// Draws an image within a region defined by a . + /// + public class ImageDrawing : Drawing + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ImageSourceProperty = + AvaloniaProperty.Register(nameof(ImageSource)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty RectProperty = + AvaloniaProperty.Register(nameof(Rect)); + + /// + /// Gets or sets the source of the image. + /// + public IImage? ImageSource + { + get => GetValue(ImageSourceProperty); + set => SetValue(ImageSourceProperty, value); + } + + /// + /// Gets or sets region in which the image is drawn. + /// + public Rect Rect + { + get => GetValue(RectProperty); + set => SetValue(RectProperty, value); + } + + public override void Draw(DrawingContext context) + { + var imageSource = ImageSource; + var rect = Rect; + + if (imageSource is object && !rect.IsEmpty) + { + context.DrawImage(imageSource, rect); + } + } + + public override Rect GetBounds() => Rect; + } +} diff --git a/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs b/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs new file mode 100644 index 0000000000..5ca21a3535 --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs @@ -0,0 +1,84 @@ +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class ImageDrawingTests : TestBase + { + public ImageDrawingTests() + : base(@"Media\ImageDrawing") + { + } + + private string BitmapPath + { + get { return System.IO.Path.Combine(OutputPath, "github_icon.png"); } + } + + [Fact] + public async Task ImageDrawing_Fill() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Image + { + Source = new DrawingImage + { + Drawing = new ImageDrawing + { + ImageSource = new Bitmap(BitmapPath), + Rect = new Rect(0, 0, 200, 200), + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task ImageDrawing_BottomRight() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Image + { + Source = new DrawingImage + { + Drawing = new DrawingGroup + { + Children = + { + new GeometryDrawing + { + Geometry = StreamGeometry.Parse("m0,0 l200,200"), + Brush = Brushes.Black, + }, + new ImageDrawing + { + ImageSource = new Bitmap(BitmapPath), + Rect = new Rect(100, 100, 100, 100), + } + } + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png b/tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..c6b7c2f3077c09d4cb50c05303f07b4c6f7c68de GIT binary patch literal 3526 zcmcgv`8O1d*H=l#no(p)WK7g!9j3mGsYOp*h-Y_QB1ax zo)I(I%5KO*M#c=XXYK1b=lvVrd(OT0+#f#o+;i`_Te79Osjz^w01pq3FbavdbELcf zV}8CPS#4KSbfk_2-7z)dsp*qt9~r!!@LO;mp1O3webN zR#%yMnUVneSs<5JAyC0tP{I7M<*ewv(j@UO#H1@vcX;UabNpYW`(UT3JP9(uj8Uq> znQBcJp6)b$K;&_uYnvN)Yg;NgEOyZ{d2#%=MYO)b+rb7M%;w!k{aYL8qX_>0i59hP zan^zQD5}`n-m{<9%Ge!aGT)xGpVzeZ+8No5|Eg)_eIFs+ul5NA@s+iL2EJ%r|Iu$j z-CevAQS0&p9SL(0a$Mu6o~KZ*_il1VZ@K7&oHJHRBoJRZ<^)b5)JW_E56$m+{$4U( zh^+hF^qI>0d>s|74_*|g;1qR;k9R!EmGJtI8v0nai0X7R^c8r%9jC8ZXuc#UsL_8r zp14!kjVe(vE@s(>cG322RVDJl{<#{Vc{73EKc(&E=jYFN9j;B#`ugyy!>-Y<1YYjX z4=I}eU&Pm1Zl!9!XRD{J#X1ZH%;;KgIvL{Ocz!n2Pj5C}P%=ft?vrKdoR(Vm300ve zuC^0>fffwJSPX@Xi6{nZONGl7yR#lug!XT`^U>3^3&5p{pxFw*+fwY!LeDZQbJfU4 ztMLICmtP|dUqa@I~5|R6ui7kS9#ud553*Kf@s@j;^vVa?NbKG(VRBCB-oN*7tFt#E z`fJB%jI~_2$znyDunN(gdd&fYrB+v0)Bi>hbH|5=MvNPK#=zelwdO506UVEG$zFYv zqcr+pnam8!epJRG>yZfl{Q78#aoE~+@N}b!u^CL%%^uA`oiOQ?z9rJ_t5}P(ogHEi zhLp3vi5iY6x&MVJyJ}I8yZ?O-wRe56?Ly20YiG8r_;kNy8~QiJ%oT*h-T@GO-k6kJ z(S27sIF5`Zr67_?p@aL}`M{P@>RYQxv^pjaT{h@8u9O_P^~H3l?M)5fqCv-8i11We}W*ej91>j;c z7_j1&Mw8hZG3~21a(W=xwrcX2qZM}_tQ{A8{umi9w@@!khMW8w7QxA!NE~^;!Kn@L zZtH9uTJPVZXhD!zv?6q(Zk?-e29nerCc*aQ6Z4}eL6GxqIc6O9t#fgI_JdOWG^ab+ zxo|Ca-Kc6|_e*2tUyU;mWpd%&YOKoK8`@`TDZNk+J}DJzoO0x^FQy?X0a44x8_8j0 zxRzBRT%yPT@dEPHbxZkR{aYJ7WKsB}$B$8eM<582$E#8V^!_&wnJ8t2BN1C17`z!v zk`uARz_CsSDbnziO-VBgML@Ux{3sYwZmxD}B2m2^9bIOXy{j()@S`tgdrDq`u+(gV zw{QOOK2_(_ROOxEtCgKI4HEw+6X=AFU3#B*1CUjNZ z`Bd8NNWAo?ASCVxtpIO5ANYY+<>lgtXKa$?t(PEH<%jxQ6F{^`^T>u^$k0%S|B`#7 zwICR4oVa_E1J)fvr|zNKtz`+#Ml=TS zHEm1=g?LBn!A26 zMRNAITyAce>Ed@2qlPNWF#jnK@iNE7Fna$m;40LI^LaBS^_kw}E(ty{XDa=wc=g_w zR|0+Ac6}F%r4UcBVolLQ%gZ(Gv1&IC7`OE{dIp5<^hgw&z#SQJ2&A4s*;S+=`l1AiR!Ws?2LeIr4HZ^dW%@gl2hW@%>+Jq4IA6FM?_(A_ z*Tr-<0uW2N1B)U}Pk6(2$)4a)>aUOA@~xB-7wp zWlaq%VgGUBhhk4ceXC)S?(nt8zvs%Hfd}gg@QT5fFZEackQh@ESUkeBd*l`C+S>~Z zU(y4g>g78@yI@-RLYgJ}oP+ID+QUp^pu0W9^ug!LgTT-A6iVpt%~2Q1w%o$*un$#i zbG|n}lNq{<#@14OE@&QS8TesIdVf>mBv)mu)Wd>0&Z1L}c@hR}8WCgf`m(uoWuQX1?FT&v0_+T$4G33TBNld13)$CMV4U#ZPGr75AFQs+I z=S4GKe7S11uVrQT^Xmlrm4C!9aiymWa9dp0u!@HqF@fo3>gyVDw`gopase{4@G9jn z_3(`0vs9f2S-}bX+j@-56o!g&^?n9WJCrJ9K?SwdI}kvzX=SybF;(?{PBa$<1y7Ipw~@;D$wBK!zDG z3rP^WBbN|z5q0qtJ9Q$`&ivo*oGtMuAmk^~3UkB!TL%%bny{w6+!5NLWvl1dArCjX zsdxNEd2hBN9H*#gr_@NHEQ#uaPuI86mxLMR##LWy1v_}4bVFN?KAU!Rd^t! zj^ED}c%Rqp8MwzoqMYb!!%~JuJZaPbXaV)~^g_x}=KfAfp^|36Iive7F=yu@E*O7w zg!Ug>KxT@f$2$|o_|G|^n70)@Yz+=+;tdvJ499jH-X=>Q%(Cuh64x6J4?nA?1vRDo z@KNSShPM?q7Ip|}UCfxYXCN; zJ>Q&uc4p>aS?s7L16utC8Z%6=$C(pJL|m81XEz5+Y0ci?_@ToF6p=pCnY!g18?2g! z?>Y`Q$O&bVa>6@rpcre{_@p!;AI6p1Q^ByB30BFykj)A;n8Q6j`cv=g7#SO)qE05L z-NLfUi?&j!50n3W7%_bkap85=^QsE~qSWAn=IkKpYgwR+4nLE`O9y>zN76A>IvEpF zK+ZL3HGK^bLEaaU532k#v{Dr3KRiHJ7zwnL(iwc-48=UmF&?^Uxs2PWGrK!j+-hFi z|MI`fO!{jN$f%iIAAyAfOLFGC{^qr}B7%-Cu`Oq(4CRT8$deO87S4e#bcMC4%cdmu z7-Pm|Jowf7#zI4fV;-9mL+YhgZ4os`p2VK_HW7o*Co8B0dM?Q#ebI!? zomI9v#zA0DXYJUL@k#7ppQ1Y(Z@OsDSAbVh~nv?9mF2e^2&^x(QlU)D4zI9TUHZy;2Ve{2Pp;&3QGq8B&l0Dx}vq)aqEi;O-L ZZGCrrDbM_k!+&oO>V`R@#^_$`{{Ri>#kv3h literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_Fill.expected.png b/tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_Fill.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..acc8532ff987e24923c908d4809d508e02e695ae GIT binary patch literal 11529 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 a*1@`ZA?>c!7F5m*BPXpaRViT_^#1{dpFcbR literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Direct2D1/Media/ImageDrawing/github_icon.png b/tests/TestFiles/Direct2D1/Media/ImageDrawing/github_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cd053c5fe1e742b21cffefd60111d8498a79dcee GIT binary patch literal 17661 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 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png b/tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a251055195f85996589719cd9e5fdec772c078 GIT binary patch literal 3183 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 literal 0 HcmV?d00001 From 4b2852f0e11e22f80a59b4e5182be5e0e090b2db Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 4 Jun 2021 14:08:30 +0200 Subject: [PATCH 018/184] Fix light dismiss overlay intercepting titlebar hit tests. --- src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index d886b67241..8a340aac5e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -117,7 +117,7 @@ namespace Avalonia.Win32 { var visual = window.Renderer.HitTestFirst(position, _owner as Window, x => { - if (x is IInputElement ie && !ie.IsHitTestVisible) + if (x is IInputElement ie && (!ie.IsHitTestVisible || !ie.IsVisible)) { return false; } From 52e6b9a6d1ac48f71de073d9cdc53bbddb5c1124 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 5 Jun 2021 22:28:20 -0400 Subject: [PATCH 019/184] Make Animation.RunAsync cancellable --- src/Avalonia.Animation/Animation.cs | 14 +++++++++----- src/Avalonia.Animation/ApiCompatBaseline.txt | 6 ++++++ src/Avalonia.Animation/IAnimation.cs | 3 ++- 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Animation/ApiCompatBaseline.txt diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index c42153ec4f..a170456854 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -3,10 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Threading; using System.Threading.Tasks; + using Avalonia.Animation.Animators; using Avalonia.Animation.Easings; -using Avalonia.Collections; using Avalonia.Data; using Avalonia.Metadata; @@ -292,7 +293,7 @@ namespace Avalonia.Animation return (newAnimatorInstances, subscriptions); } - /// + /// public IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete) { var (animators, subscriptions) = InterpretKeyframes(control); @@ -323,21 +324,24 @@ namespace Avalonia.Animation return new CompositeDisposable(subscriptions); } - /// - public Task RunAsync(Animatable control, IClock clock = null) + /// + public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default) { var run = new TaskCompletionSource(); if (this.IterationCount == IterationCount.Infinite) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); - IDisposable subscriptions = null; + IDisposable subscriptions = null, cancellation = null; subscriptions = this.Apply(control, clock, Observable.Return(true), () => { run.SetResult(null); subscriptions?.Dispose(); + cancellation?.Dispose(); }); + cancellation = cancellationToken.Register(state => ((IDisposable)state).Dispose(), subscriptions); + return run.Task; } } diff --git a/src/Avalonia.Animation/ApiCompatBaseline.txt b/src/Avalonia.Animation/ApiCompatBaseline.txt new file mode 100644 index 0000000000..58cb7830e7 --- /dev/null +++ b/src/Avalonia.Animation/ApiCompatBaseline.txt @@ -0,0 +1,6 @@ +Compat issues with assembly Avalonia.Animation: +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.Animation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock, System.Threading.CancellationToken)' is present in the implementation but not in the contract. +Total Issues: 4 diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index ff85535d8a..5844ba5688 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace Avalonia.Animation @@ -16,6 +17,6 @@ namespace Avalonia.Animation /// /// Run the animation on the specified control. /// - Task RunAsync(Animatable control, IClock clock); + Task RunAsync(Animatable control, IClock clock, CancellationToken cancellationToken); } } From 4bbedf581562fcf14a5a4227f135876db3f8856f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 5 Jun 2021 22:28:45 -0400 Subject: [PATCH 020/184] Make PageTransition.Start cancellable --- .../Animation/CompositePageTransition.cs | 21 ++---- src/Avalonia.Visuals/Animation/CrossFade.cs | 64 ++++++++----------- .../Animation/IPageTransition.cs | 6 +- src/Avalonia.Visuals/Animation/PageSlide.cs | 30 ++++----- src/Avalonia.Visuals/ApiCompatBaseline.txt | 9 ++- 5 files changed, 55 insertions(+), 75 deletions(-) diff --git a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs index 9489914c97..2deebd7792 100644 --- a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs +++ b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Avalonia.Metadata; @@ -35,25 +36,11 @@ namespace Avalonia.Animation [Content] public List PageTransitions { get; set; } = new List(); - /// - /// Starts the animation. - /// - /// - /// The control that is being transitioned away from. May be null. - /// - /// - /// The control that is being transitioned to. May be null. - /// - /// - /// Defines the direction of the transition. - /// - /// - /// A that tracks the progress of the animation. - /// - public Task Start(Visual from, Visual to, bool forward) + /// + public Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken) { var transitionTasks = PageTransitions - .Select(transition => transition.Start(from, to, forward)) + .Select(transition => transition.Start(from, to, forward, cancellationToken)) .ToList(); return Task.WhenAll(transitionTasks); } diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 0615b854da..9ff0d99b23 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Threading; using System.Threading.Tasks; using Avalonia.Animation.Easings; using Avalonia.Styling; @@ -97,49 +99,39 @@ namespace Avalonia.Animation set => _fadeOutAnimation.Easing = value; } - /// - /// Starts the animation. - /// - /// - /// The control that is being transitioned away from. May be null. - /// - /// - /// The control that is being transitioned to. May be null. - /// - /// - /// A that tracks the progress of the animation. - /// - public async Task Start(Visual from, Visual to) + /// + public async Task Start(Visual from, Visual to, CancellationToken cancellationToken) { - var tasks = new List(); - - if (to != null) - { - to.Opacity = 0; - } - - if (from != null) + if (cancellationToken.IsCancellationRequested) { - tasks.Add(_fadeOutAnimation.RunAsync(from)); + return; } - if (to != null) + var tasks = new List(); + using (var disposables = new CompositeDisposable()) { - to.IsVisible = true; - tasks.Add(_fadeInAnimation.RunAsync(to)); + if (to != null) + { + disposables.Add(to.SetValue(Visual.OpacityProperty, 0, Data.BindingPriority.Animation)); + } - } + if (from != null) + { + tasks.Add(_fadeOutAnimation.RunAsync(from, null, cancellationToken)); + } - await Task.WhenAll(tasks); + if (to != null) + { + to.IsVisible = true; + tasks.Add(_fadeInAnimation.RunAsync(to, null, cancellationToken)); + } - if (from != null) - { - from.IsVisible = false; - } + await Task.WhenAll(tasks); - if (to != null) - { - to.Opacity = 1; + if (from != null && !cancellationToken.IsCancellationRequested) + { + from.IsVisible = false; + } } } @@ -158,9 +150,9 @@ namespace Avalonia.Animation /// /// A that tracks the progress of the animation. /// - Task IPageTransition.Start(Visual from, Visual to, bool forward) + Task IPageTransition.Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken) { - return Start(from, to); + return Start(from, to, cancellationToken); } } } diff --git a/src/Avalonia.Visuals/Animation/IPageTransition.cs b/src/Avalonia.Visuals/Animation/IPageTransition.cs index 659bc12424..2d19ddbb5b 100644 --- a/src/Avalonia.Visuals/Animation/IPageTransition.cs +++ b/src/Avalonia.Visuals/Animation/IPageTransition.cs @@ -1,3 +1,4 @@ +using System.Threading; using System.Threading.Tasks; namespace Avalonia.Animation @@ -19,9 +20,12 @@ namespace Avalonia.Animation /// /// If the animation is bidirectional, controls the direction of the animation. /// + /// + /// Animation cancellation. + /// /// /// A that tracks the progress of the animation. /// - Task Start(Visual from, Visual to, bool forward); + Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken); } } diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs index dd5d598e12..7d033ccf61 100644 --- a/src/Avalonia.Visuals/Animation/PageSlide.cs +++ b/src/Avalonia.Visuals/Animation/PageSlide.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Avalonia.Animation.Easings; using Avalonia.Media; @@ -60,23 +61,14 @@ namespace Avalonia.Animation /// public Easing SlideOutEasing { get; set; } = new LinearEasing(); - /// - /// Starts the animation. - /// - /// - /// The control that is being transitioned away from. May be null. - /// - /// - /// The control that is being transitioned to. May be null. - /// - /// - /// If true, the new page is slid in from the right, or if false from the left. - /// - /// - /// A that tracks the progress of the animation. - /// - public async Task Start(Visual from, Visual to, bool forward) + /// + public async Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + return; + } + var tasks = new List(); var parent = GetVisualParent(from, to); var distance = Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height; @@ -109,7 +101,7 @@ namespace Avalonia.Animation }, Duration = Duration }; - tasks.Add(animation.RunAsync(from)); + tasks.Add(animation.RunAsync(from, null, cancellationToken)); } if (to != null) @@ -140,12 +132,12 @@ namespace Avalonia.Animation }, Duration = Duration }; - tasks.Add(animation.RunAsync(to)); + tasks.Add(animation.RunAsync(to, null, cancellationToken)); } await Task.WhenAll(tasks); - if (from != null) + if (from != null && !cancellationToken.IsCancellationRequested) { from.IsVisible = false; } diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index f9fd125615..c917902dc3 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -1,4 +1,10 @@ Compat issues with assembly Avalonia.Visuals: +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.CompositePageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.CrossFade.Start(Avalonia.Visual, Avalonia.Visual)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. @@ -63,9 +69,8 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalon InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation. MembersMustExist : Member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' does not exist in the implementation but it does exist in the contract. -Total Issues: 64 InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.IO.Stream)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. -Total Issues: 11 +Total Issues: 74 From f13ece461b43306c8f9c2680662e649c265b1e07 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 5 Jun 2021 22:29:16 -0400 Subject: [PATCH 021/184] Provide cancellation to animations where neccessary --- src/Avalonia.Controls/Expander.cs | 21 +++++++++++++------ .../Presenters/CarouselPresenter.cs | 2 +- .../TransitioningContentControl.cs | 13 +++++++++--- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 052b42a233..b9c79e5749 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -1,7 +1,11 @@ +using System.Threading; + using Avalonia.Animation; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +#nullable enable + namespace Avalonia.Controls { /// @@ -36,8 +40,8 @@ namespace Avalonia.Controls [PseudoClasses(":expanded", ":up", ":down", ":left", ":right")] public class Expander : HeaderedContentControl { - public static readonly StyledProperty ContentTransitionProperty = - AvaloniaProperty.Register(nameof(ContentTransition)); + public static readonly StyledProperty ContentTransitionProperty = + AvaloniaProperty.Register(nameof(ContentTransition)); public static readonly StyledProperty ExpandDirectionProperty = AvaloniaProperty.Register(nameof(ExpandDirection), ExpandDirection.Down); @@ -50,6 +54,7 @@ namespace Avalonia.Controls defaultBindingMode: Data.BindingMode.TwoWay); private bool _isExpanded; + private CancellationTokenSource? _lastTransitionCts; static Expander() { @@ -61,7 +66,7 @@ namespace Avalonia.Controls UpdatePseudoClasses(ExpandDirection); } - public IPageTransition ContentTransition + public IPageTransition? ContentTransition { get => GetValue(ContentTransitionProperty); set => SetValue(ContentTransitionProperty, value); @@ -83,19 +88,23 @@ namespace Avalonia.Controls } } - protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e) + protected virtual async void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e) { if (Content != null && ContentTransition != null && Presenter is Visual visualContent) { bool forward = ExpandDirection == ExpandDirection.Left || ExpandDirection == ExpandDirection.Up; + + _lastTransitionCts?.Cancel(); + _lastTransitionCts = new CancellationTokenSource(); + if (IsExpanded) { - ContentTransition.Start(null, visualContent, forward); + await ContentTransition.Start(null, visualContent, forward, _lastTransitionCts.Token); } else { - ContentTransition.Start(visualContent, null, !forward); + await ContentTransition.Start(visualContent, null, forward, _lastTransitionCts.Token); } } } diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 7888249bdd..81f43865a7 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -186,7 +186,7 @@ namespace Avalonia.Controls.Presenters if (PageTransition != null && (from != null || to != null)) { - await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex); + await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex, default); } else if (to != null) { diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index 9685ecbe91..c4dd79f468 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; + using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Styling; @@ -22,7 +24,9 @@ namespace Avalonia.ReactiveUI /// public static readonly StyledProperty DefaultContentProperty = AvaloniaProperty.Register(nameof(DefaultContent)); - + + private CancellationTokenSource? _lastTransitionCts; + /// /// Gets or sets the animation played when content appears and disappears. /// @@ -62,11 +66,14 @@ namespace Avalonia.ReactiveUI /// New content to set. private async void UpdateContentWithTransition(object? content) { + _lastTransitionCts?.Cancel(); + _lastTransitionCts = new CancellationTokenSource(); + if (PageTransition != null) - await PageTransition.Start(this, null, true); + await PageTransition.Start(this, null, true, _lastTransitionCts.Token); base.Content = content; if (PageTransition != null) - await PageTransition.Start(null, this, true); + await PageTransition.Start(null, this, true, _lastTransitionCts.Token); } } } From 9da802d4845a8507b586926fb7ca4e65c1d84ce2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 5 Jun 2021 22:38:19 -0400 Subject: [PATCH 022/184] Do not run animation if it was cancelled --- src/Avalonia.Animation/Animation.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index a170456854..eb48fd7b16 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -327,6 +327,11 @@ namespace Avalonia.Animation /// public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return Task.CompletedTask; + } + var run = new TaskCompletionSource(); if (this.IterationCount == IterationCount.Infinite) From 471aa9c7779c6d0e29474d34dc1c885863799021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 6 Jun 2021 15:06:49 +0200 Subject: [PATCH 023/184] fix xaml errors in filtertextbox --- .../Diagnostics/Controls/FilterTextBox.axaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml index 52720e652f..b7995c38e3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml @@ -27,7 +27,9 @@ - M7.495 9.052L8.386 11.402H9.477L6.237 3H5.217L2 11.402H3.095L3.933 9.052H7.495ZM5.811 4.453L5.855 4.588L7.173 8.162H4.255L5.562 4.588L5.606 4.453L5.644 4.297L5.676 4.145L5.697 4.019H5.72L5.744 4.145L5.773 4.297L5.811 4.453ZM13.795 10.464V11.4H14.755V7.498C14.755 6.779 14.575 6.226 14.216 5.837C13.857 5.448 13.327 5.254 12.628 5.254C12.429 5.254 12.227 5.273 12.022 5.31C11.817 5.347 11.622 5.394 11.439 5.451C11.256 5.508 11.091 5.569 10.944 5.636C10.797 5.703 10.683 5.765 10.601 5.824V6.808C10.867 6.578 11.167 6.397 11.505 6.268C11.843 6.139 12.194 6.075 12.557 6.075C12.745 6.075 12.915 6.103 13.07 6.16C13.225 6.217 13.357 6.306 13.466 6.427C13.575 6.548 13.659 6.706 13.718 6.899C13.777 7.092 13.806 7.326 13.806 7.599L11.995 7.851C11.651 7.898 11.355 7.977 11.107 8.088C10.859 8.199 10.654 8.339 10.492 8.507C10.33 8.675 10.21 8.868 10.132 9.087C10.054 9.306 10.015 9.546 10.015 9.808C10.015 10.054 10.057 10.283 10.139 10.496C10.221 10.709 10.342 10.893 10.502 11.047C10.662 11.201 10.862 11.323 11.1 11.413C11.338 11.503 11.613 11.548 11.926 11.548C12.328 11.548 12.686 11.456 13.001 11.27C13.316 11.084 13.573 10.816 13.772 10.464H13.795ZM11.667 8.721C11.843 8.657 12.068 8.607 12.341 8.572L13.806 8.367V8.976C13.806 9.222 13.765 9.451 13.683 9.664C13.601 9.877 13.486 10.063 13.34 10.221C13.194 10.379 13.019 10.503 12.816 10.593C12.613 10.683 12.39 10.728 12.148 10.728C11.961 10.728 11.795 10.703 11.653 10.652C11.511 10.601 11.392 10.53 11.296 10.441C11.2 10.352 11.127 10.247 11.076 10.125C11.025 10.003 11 9.873 11 9.732C11 9.568 11.018 9.421 11.055 9.292C11.092 9.163 11.16 9.051 11.257 8.958C11.354 8.865 11.491 8.785 11.667 8.721Z + + M7.495 9.052L8.386 11.402H9.477L6.237 3H5.217L2 11.402H3.095L3.933 9.052H7.495ZM5.811 4.453L5.855 4.588L7.173 8.162H4.255L5.562 4.588L5.606 4.453L5.644 4.297L5.676 4.145L5.697 4.019H5.72L5.744 4.145L5.773 4.297L5.811 4.453ZM13.795 10.464V11.4H14.755V7.498C14.755 6.779 14.575 6.226 14.216 5.837C13.857 5.448 13.327 5.254 12.628 5.254C12.429 5.254 12.227 5.273 12.022 5.31C11.817 5.347 11.622 5.394 11.439 5.451C11.256 5.508 11.091 5.569 10.944 5.636C10.797 5.703 10.683 5.765 10.601 5.824V6.808C10.867 6.578 11.167 6.397 11.505 6.268C11.843 6.139 12.194 6.075 12.557 6.075C12.745 6.075 12.915 6.103 13.07 6.16C13.225 6.217 13.357 6.306 13.466 6.427C13.575 6.548 13.659 6.706 13.718 6.899C13.777 7.092 13.806 7.326 13.806 7.599L11.995 7.851C11.651 7.898 11.355 7.977 11.107 8.088C10.859 8.199 10.654 8.339 10.492 8.507C10.33 8.675 10.21 8.868 10.132 9.087C10.054 9.306 10.015 9.546 10.015 9.808C10.015 10.054 10.057 10.283 10.139 10.496C10.221 10.709 10.342 10.893 10.502 11.047C10.662 11.201 10.862 11.323 11.1 11.413C11.338 11.503 11.613 11.548 11.926 11.548C12.328 11.548 12.686 11.456 13.001 11.27C13.316 11.084 13.573 10.816 13.772 10.464H13.795ZM11.667 8.721C11.843 8.657 12.068 8.607 12.341 8.572L13.806 8.367V8.976C13.806 9.222 13.765 9.451 13.683 9.664C13.601 9.877 13.486 10.063 13.34 10.221C13.194 10.379 13.019 10.503 12.816 10.593C12.613 10.683 12.39 10.728 12.148 10.728C11.961 10.728 11.795 10.703 11.653 10.652C11.511 10.601 11.392 10.53 11.296 10.441C11.2 10.352 11.127 10.247 11.076 10.125C11.025 10.003 11 9.873 11 9.732C11 9.568 11.018 9.421 11.055 9.292C11.092 9.163 11.16 9.051 11.257 8.958C11.354 8.865 11.491 8.785 11.667 8.721Z + @@ -38,7 +40,9 @@ - M1 2H15V3H1V2ZM14 4H13V12H14V4ZM11.272 8.387C11.194 8.088 11.073 7.825 10.912 7.601C10.751 7.377 10.547 7.2 10.303 7.071C10.059 6.942 9.769 6.878 9.437 6.878C9.239 6.878 9.057 6.902 8.89 6.951C8.725 7 8.574 7.068 8.437 7.156C8.301 7.244 8.18 7.35 8.072 7.474L7.893 7.732V4.578H7V12H7.893V11.425L8.019 11.6C8.106 11.702 8.208 11.79 8.323 11.869C8.44 11.947 8.572 12.009 8.721 12.055C8.87 12.101 9.035 12.123 9.219 12.123C9.572 12.123 9.885 12.052 10.156 11.911C10.428 11.768 10.655 11.573 10.838 11.325C11.021 11.075 11.159 10.782 11.252 10.446C11.345 10.108 11.392 9.743 11.392 9.349C11.391 9.007 11.352 8.686 11.272 8.387ZM9.793 7.78C9.944 7.851 10.075 7.956 10.183 8.094C10.292 8.234 10.377 8.407 10.438 8.611C10.489 8.785 10.52 8.982 10.527 9.198L10.52 9.323C10.52 9.65 10.487 9.943 10.42 10.192C10.353 10.438 10.259 10.645 10.142 10.806C10.025 10.968 9.882 11.091 9.721 11.172C9.399 11.334 8.961 11.338 8.652 11.187C8.499 11.112 8.366 11.012 8.259 10.891C8.174 10.795 8.103 10.675 8.041 10.524C8.041 10.524 7.862 10.077 7.862 9.577C7.862 9.077 8.041 8.575 8.041 8.575C8.103 8.398 8.177 8.257 8.265 8.145C8.379 8.002 8.521 7.886 8.689 7.8C8.857 7.714 9.054 7.671 9.276 7.671C9.466 7.671 9.64 7.708 9.793 7.78ZM15 13H1V14H15V13ZM2.813 10L2.085 12.031H1L1.025 11.959L3.466 4.87305H4.407L6.892 12.031H5.81L5.032 10H2.813ZM3.934 6.42205H3.912L3.007 9.17505H4.848L3.934 6.42205Z + + M1 2H15V3H1V2ZM14 4H13V12H14V4ZM11.272 8.387C11.194 8.088 11.073 7.825 10.912 7.601C10.751 7.377 10.547 7.2 10.303 7.071C10.059 6.942 9.769 6.878 9.437 6.878C9.239 6.878 9.057 6.902 8.89 6.951C8.725 7 8.574 7.068 8.437 7.156C8.301 7.244 8.18 7.35 8.072 7.474L7.893 7.732V4.578H7V12H7.893V11.425L8.019 11.6C8.106 11.702 8.208 11.79 8.323 11.869C8.44 11.947 8.572 12.009 8.721 12.055C8.87 12.101 9.035 12.123 9.219 12.123C9.572 12.123 9.885 12.052 10.156 11.911C10.428 11.768 10.655 11.573 10.838 11.325C11.021 11.075 11.159 10.782 11.252 10.446C11.345 10.108 11.392 9.743 11.392 9.349C11.391 9.007 11.352 8.686 11.272 8.387ZM9.793 7.78C9.944 7.851 10.075 7.956 10.183 8.094C10.292 8.234 10.377 8.407 10.438 8.611C10.489 8.785 10.52 8.982 10.527 9.198L10.52 9.323C10.52 9.65 10.487 9.943 10.42 10.192C10.353 10.438 10.259 10.645 10.142 10.806C10.025 10.968 9.882 11.091 9.721 11.172C9.399 11.334 8.961 11.338 8.652 11.187C8.499 11.112 8.366 11.012 8.259 10.891C8.174 10.795 8.103 10.675 8.041 10.524C8.041 10.524 7.862 10.077 7.862 9.577C7.862 9.077 8.041 8.575 8.041 8.575C8.103 8.398 8.177 8.257 8.265 8.145C8.379 8.002 8.521 7.886 8.689 7.8C8.857 7.714 9.054 7.671 9.276 7.671C9.466 7.671 9.64 7.708 9.793 7.78ZM15 13H1V14H15V13ZM2.813 10L2.085 12.031H1L1.025 11.959L3.466 4.87305H4.407L6.892 12.031H5.81L5.032 10H2.813ZM3.934 6.42205H3.912L3.007 9.17505H4.848L3.934 6.42205Z + @@ -49,7 +53,9 @@ - M10.0122 2H10.9879V5.11346L13.5489 3.55609L14.034 4.44095L11.4702 6L14.034 7.55905L13.5489 8.44391L10.9879 6.88654V10H10.0122V6.88654L7.45114 8.44391L6.96606 7.55905L9.5299 6L6.96606 4.44095L7.45114 3.55609L10.0122 5.11346V2ZM2 10H6V14H2V10Z + + M10.0122 2H10.9879V5.11346L13.5489 3.55609L14.034 4.44095L11.4702 6L14.034 7.55905L13.5489 8.44391L10.9879 6.88654V10H10.0122V6.88654L7.45114 8.44391L6.96606 7.55905L9.5299 6L6.96606 4.44095L7.45114 3.55609L10.0122 5.11346V2ZM2 10H6V14H2V10Z + From dbe9a2e2d17cbcd8ba5e1ea960b5e74a3564a31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 6 Jun 2021 15:07:13 +0200 Subject: [PATCH 024/184] fix xaml errors in controlcatalog sample --- samples/ControlCatalog/Pages/ContextFlyoutPage.axaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml index e15637aa0f..f0e079ad91 100644 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml @@ -84,13 +84,13 @@ - - - From 12c37270eeedb6fecf620f06a74e85b60514f3ac Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Sun, 6 Jun 2021 12:27:14 -0400 Subject: [PATCH 025/184] SelectedDate and SelectedTime => TwoWay by default Fixes #3407 --- src/Avalonia.Controls/DateTimePickers/DatePicker.cs | 4 +++- src/Avalonia.Controls/DateTimePickers/TimePicker.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index 8d893154eb..43bc7d1df9 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Interactivity; using System; using System.Collections.Generic; @@ -88,7 +89,8 @@ namespace Avalonia.Controls /// public static readonly DirectProperty SelectedDateProperty = AvaloniaProperty.RegisterDirect(nameof(SelectedDate), - x => x.SelectedDate, (x, v) => x.SelectedDate = v); + x => x.SelectedDate, (x, v) => x.SelectedDate = v, + defaultBindingMode: BindingMode.TwoWay); // Template Items private Button _flyoutButton; diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index d0cf772c01..6b3f66912f 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Data; using System; using System.Globalization; @@ -44,7 +45,8 @@ namespace Avalonia.Controls /// public static readonly DirectProperty SelectedTimeProperty = AvaloniaProperty.RegisterDirect(nameof(SelectedTime), - x => x.SelectedTime, (x, v) => x.SelectedTime = v); + x => x.SelectedTime, (x, v) => x.SelectedTime = v, + defaultBindingMode: BindingMode.TwoWay); // Template Items private TimePickerPresenter _presenter; From 6763337e25befce9c77e1a51da2cf57556fbfd0e Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Sun, 6 Jun 2021 12:45:53 -0400 Subject: [PATCH 026/184] Add IsUndoEnabled to TextBox --- src/Avalonia.Controls/TextBox.cs | 87 +++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 3c221cbf27..e38531e540 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -119,9 +119,9 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(RevealPassword)); public static readonly DirectProperty CanCutProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanCut), - o => o.CanCut); + AvaloniaProperty.RegisterDirect( + nameof(CanCut), + o => o.CanCut); public static readonly DirectProperty CanCopyProperty = AvaloniaProperty.RegisterDirect( @@ -129,9 +129,14 @@ namespace Avalonia.Controls o => o.CanCopy); public static readonly DirectProperty CanPasteProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanPaste), - o => o.CanPaste); + AvaloniaProperty.RegisterDirect( + nameof(CanPaste), + o => o.CanPaste); + + public static readonly StyledProperty IsUndoEnabledProperty = + AvaloniaProperty.Register( + nameof(IsUndoEnabled), + defaultValue: true); struct UndoRedoState : IEquatable { @@ -218,7 +223,7 @@ namespace Avalonia.Controls value = CoerceCaretIndex(value); SetAndRaise(CaretIndexProperty, ref _caretIndex, value); UndoRedoState state; - if (_undoRedoHelper.TryGetLastState(out state) && state.Text == Text) + if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) _undoRedoHelper.UpdateLastState(); } } @@ -316,7 +321,7 @@ namespace Avalonia.Controls SelectionEnd = CoerceCaretIndex(SelectionEnd, value); CaretIndex = CoerceCaretIndex(caretIndex, value); - if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing) + if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) { _undoRedoHelper.Clear(); } @@ -329,7 +334,7 @@ namespace Avalonia.Controls get { return GetSelection(); } set { - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); if (string.IsNullOrEmpty(value)) { DeleteSelection(); @@ -338,7 +343,7 @@ namespace Avalonia.Controls { HandleTextInput(value); } - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); } } @@ -446,6 +451,27 @@ namespace Avalonia.Controls private set { SetAndRaise(CanPasteProperty, ref _canPaste, value); } } + /// + /// Property for determining whether undo/redo is enabled + /// + public bool IsUndoEnabled + { + get { return GetValue(IsUndoEnabledProperty); } + set + { + SetValue(IsUndoEnabledProperty, value); + if (value == false) + { + // from docs at + // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: + // "Setting this property to false clears the undo stack. + // Therefore, if you disable undo and then re-enable it, undo commands still do not work + // because the undo stack was emptied when you disabled undo." + _undoRedoHelper.Clear(); + } + } + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); @@ -551,7 +577,10 @@ namespace Avalonia.Controls SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); CaretIndex += input.Length; ClearSelection(); - _undoRedoHelper.DiscardRedo(); + if (IsUndoEnabled) + { + _undoRedoHelper.DiscardRedo(); + } } } @@ -570,10 +599,10 @@ namespace Avalonia.Controls var text = GetSelection(); if (text is null) return; - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); Copy(); DeleteSelection(); - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); } public async void Copy() @@ -591,9 +620,9 @@ namespace Avalonia.Controls if (text is null) return; - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); HandleTextInput(text); - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); } protected override void OnKeyDown(KeyEventArgs e) @@ -638,7 +667,7 @@ namespace Avalonia.Controls Paste(); handled = true; } - else if (Match(keymap.Undo)) + else if (Match(keymap.Undo) && IsUndoEnabled) { try { @@ -652,7 +681,7 @@ namespace Avalonia.Controls handled = true; } - else if (Match(keymap.Redo)) + else if (Match(keymap.Redo) && IsUndoEnabled) { try { @@ -752,7 +781,7 @@ namespace Avalonia.Controls break; case Key.Back: - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlBackspace(); @@ -776,13 +805,13 @@ namespace Avalonia.Controls CaretIndex -= removedCharacters; ClearSelection(); } - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); handled = true; break; case Key.Delete: - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlDelete(); @@ -804,7 +833,7 @@ namespace Avalonia.Controls SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters)); } - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); handled = true; break; @@ -812,9 +841,9 @@ namespace Avalonia.Controls case Key.Enter: if (AcceptsReturn) { - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); HandleTextInput(NewLine); - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); handled = true; } @@ -823,9 +852,9 @@ namespace Avalonia.Controls case Key.Tab: if (AcceptsTab) { - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); HandleTextInput("\t"); - _undoRedoHelper.Snapshot(); + SnapshotUndoRedo(); handled = true; } else @@ -1251,5 +1280,13 @@ namespace Avalonia.Controls ClearSelection(); } } + + private void SnapshotUndoRedo() + { + if (IsUndoEnabled) + { + _undoRedoHelper.Snapshot(); + } + } } } From c6eb57c5350cf2e236f6c3c8bc26297b24d70ef7 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Sun, 6 Jun 2021 13:19:46 -0400 Subject: [PATCH 027/184] Add TextBox UndoLimit --- src/Avalonia.Controls/TextBox.cs | 20 +++++++++++++++++++ src/Avalonia.Controls/Utils/UndoRedoHelper.cs | 11 ++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index e38531e540..ffe0d39cab 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -138,6 +138,13 @@ namespace Avalonia.Controls nameof(IsUndoEnabled), defaultValue: true); + public static readonly DirectProperty UndoLimitProperty = + AvaloniaProperty.RegisterDirect( + nameof(UndoLimit), + o => o._undoRedoHelper.Limit, + (o, v) => o._undoRedoHelper.Limit = v, + unsetValue: -1); + struct UndoRedoState : IEquatable { public string Text { get; } @@ -472,6 +479,19 @@ namespace Avalonia.Controls } } + public int UndoLimit + { + get { return GetValue(UndoLimitProperty); } + set + { + SetValue(UndoLimitProperty, value); + // from docs at + // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: + // "Setting UndoLimit clears the undo queue." + _undoRedoHelper.Clear(); + } + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); diff --git a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs index 17cf681f15..7374f20a0c 100644 --- a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs +++ b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs @@ -22,6 +22,10 @@ namespace Avalonia.Controls.Utils private LinkedListNode _currentNode; + /// + /// Maximum number of states this helper can store for undo/redo. + /// If -1, no limit is imposed. + /// public int Limit { get; set; } = 10; public UndoRedoHelper(IUndoRedoHost host) @@ -54,7 +58,10 @@ namespace Avalonia.Controls.Utils public bool HasState => _currentNode != null; public void UpdateLastState(TState state) { - _states.Last.Value = state; + if (_states.Last != null) + { + _states.Last.Value = state; + } } public void UpdateLastState() @@ -86,7 +93,7 @@ namespace Avalonia.Controls.Utils DiscardRedo(); _states.AddLast(current); _currentNode = _states.Last; - if (_states.Count > Limit) + if (Limit != -1 && _states.Count > Limit) _states.RemoveFirst(); } } From 4fe29b0f9e9568a85f0ced91e5041e80ee55d2c5 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Sun, 6 Jun 2021 21:27:47 -0400 Subject: [PATCH 028/184] UndoLimit now uses property properly --- src/Avalonia.Controls/TextBox.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index ffe0d39cab..06745bb3bc 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -141,8 +141,8 @@ namespace Avalonia.Controls public static readonly DirectProperty UndoLimitProperty = AvaloniaProperty.RegisterDirect( nameof(UndoLimit), - o => o._undoRedoHelper.Limit, - (o, v) => o._undoRedoHelper.Limit = v, + o => o.UndoLimit, + (o, v) => o.UndoLimit = v, unsetValue: -1); struct UndoRedoState : IEquatable @@ -481,10 +481,10 @@ namespace Avalonia.Controls public int UndoLimit { - get { return GetValue(UndoLimitProperty); } + get { return _undoRedoHelper.Limit; } set { - SetValue(UndoLimitProperty, value); + _undoRedoHelper.Limit = value; // from docs at // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: // "Setting UndoLimit clears the undo queue." From b5bb89348df5bdd201664c6e3ef71a5ed13400fe Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Sun, 6 Jun 2021 21:38:50 -0400 Subject: [PATCH 029/184] Handle IsUndoEnabled in property changed --- src/Avalonia.Controls/TextBox.cs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 06745bb3bc..351d648afc 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -464,19 +464,7 @@ namespace Avalonia.Controls public bool IsUndoEnabled { get { return GetValue(IsUndoEnabledProperty); } - set - { - SetValue(IsUndoEnabledProperty, value); - if (value == false) - { - // from docs at - // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: - // "Setting this property to false clears the undo stack. - // Therefore, if you disable undo and then re-enable it, undo commands still do not work - // because the undo stack was emptied when you disabled undo." - _undoRedoHelper.Clear(); - } - } + set { SetValue(IsUndoEnabledProperty, value); } } public int UndoLimit @@ -511,6 +499,15 @@ namespace Avalonia.Controls UpdatePseudoclasses(); UpdateCommandStates(); } + else if (change.Property == IsUndoEnabledProperty && change.NewValue.GetValueOrDefault() == false) + { + // from docs at + // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: + // "Setting this property to false clears the undo stack. + // Therefore, if you disable undo and then re-enable it, undo commands still do not work + // because the undo stack was emptied when you disabled undo." + _undoRedoHelper.Clear(); + } } private void UpdateCommandStates() From 8ed27724793cd7315c9fd2cd73f804a9429ab1ae Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Mon, 7 Jun 2021 07:56:35 -0400 Subject: [PATCH 030/184] Fix not raising property change on UndoLimit --- src/Avalonia.Controls/TextBox.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 351d648afc..1bee15bccd 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -472,7 +472,15 @@ namespace Avalonia.Controls get { return _undoRedoHelper.Limit; } set { - _undoRedoHelper.Limit = value; + if (_undoRedoHelper.Limit != value) + { + // can't use SetAndRaise due to using _undoRedoHelper.Limit + // (can't send a ref of a property to SetAndRaise), + // so use RaisePropertyChanged instead. + var oldValue = _undoRedoHelper.Limit; + _undoRedoHelper.Limit = value; + RaisePropertyChanged(UndoLimitProperty, oldValue, value); + } // from docs at // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: // "Setting UndoLimit clears the undo queue." From 5365b2fc9564d72d01873a06f6ec79684ef0d7e9 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Jun 2021 16:04:06 +0200 Subject: [PATCH 031/184] Transitions for solid color brushes. --- samples/RenderDemo/Pages/TransitionsPage.xaml | 15 ++++++++++ .../Animation/Transitions/BrushTransition.cs | 29 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml index f9f69fb341..ce5db380c0 100644 --- a/samples/RenderDemo/Pages/TransitionsPage.xaml +++ b/samples/RenderDemo/Pages/TransitionsPage.xaml @@ -141,6 +141,19 @@ + + + + @@ -166,6 +179,8 @@ + + diff --git a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs new file mode 100644 index 0000000000..0f7501e279 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs @@ -0,0 +1,29 @@ +using System; +using Avalonia.Animation.Animators; +using Avalonia.Media; + +namespace Avalonia.Animation +{ + /// + /// Transition class that handles with type. + /// Only values of will correctly transition. + /// + public class ISolidColorBrushTransition : Transition + { + private static readonly ISolidColorBrushAnimator s_animator = new ISolidColorBrushAnimator(); + + public override IObservable DoTransition(IObservable progress, IBrush oldValue, IBrush newValue) + { + var oldSolidBrush = AsImmutable(oldValue); + var newSolidBrush = AsImmutable(newValue); + + return new AnimatorTransitionObservable( + s_animator, progress, Easing, oldSolidBrush, newSolidBrush); + } + + private static ISolidColorBrush AsImmutable(IBrush brush) + { + return (ISolidColorBrush)(brush as ISolidColorBrush)?.ToImmutable(); + } + } +} From c48a516ec6e30c6f2e6ebe52e0e6c06c62157c8a Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Jun 2021 23:33:55 +0200 Subject: [PATCH 032/184] Unify brush transition. Handle brush type mismatch in a nicer way. --- samples/RenderDemo/Pages/TransitionsPage.xaml | 23 +++++++- .../Animation/Transitions/BrushTransition.cs | 56 ++++++++++++++++--- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml index ce5db380c0..1985074b0f 100644 --- a/samples/RenderDemo/Pages/TransitionsPage.xaml +++ b/samples/RenderDemo/Pages/TransitionsPage.xaml @@ -145,7 +145,7 @@ + + + + @@ -181,6 +201,7 @@ + diff --git a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs index 0f7501e279..8d0b9089f9 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs @@ -1,29 +1,67 @@ using System; using Avalonia.Animation.Animators; +using Avalonia.Animation.Easings; using Avalonia.Media; +#nullable enable + namespace Avalonia.Animation { /// /// Transition class that handles with type. - /// Only values of will correctly transition. + /// Only values of will transition correctly at the moment. /// - public class ISolidColorBrushTransition : Transition + public class BrushTransition : Transition { private static readonly ISolidColorBrushAnimator s_animator = new ISolidColorBrushAnimator(); - public override IObservable DoTransition(IObservable progress, IBrush oldValue, IBrush newValue) + public override IObservable DoTransition(IObservable progress, IBrush? oldValue, IBrush? newValue) + { + var oldSolidColorBrush = TryGetSolidColorBrush(oldValue); + var newSolidColorBrush = TryGetSolidColorBrush(newValue); + + if (oldSolidColorBrush != null && newSolidColorBrush != null) + { + EnsureImmutable(ref oldSolidColorBrush); + EnsureImmutable(ref newSolidColorBrush); + + return new AnimatorTransitionObservable( + s_animator, progress, Easing, oldSolidColorBrush, newSolidColorBrush); + } + + return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); + } + + private static void EnsureImmutable(ref ISolidColorBrush brush) + { + brush = (ISolidColorBrush)brush.ToImmutable(); + } + + private static ISolidColorBrush? TryGetSolidColorBrush(IBrush? brush) { - var oldSolidBrush = AsImmutable(oldValue); - var newSolidBrush = AsImmutable(newValue); + if (brush is null) + { + return Brushes.Transparent; + } - return new AnimatorTransitionObservable( - s_animator, progress, Easing, oldSolidBrush, newSolidBrush); + return brush as ISolidColorBrush; } - private static ISolidColorBrush AsImmutable(IBrush brush) + private class IncompatibleTransitionObservable : TransitionObservableBase { - return (ISolidColorBrush)(brush as ISolidColorBrush)?.ToImmutable(); + private readonly IBrush? _from; + private readonly IBrush? _to; + + public IncompatibleTransitionObservable(IObservable progress, Easing easing, IBrush? from, IBrush? to) : base(progress, easing) + { + _from = @from; + _to = to; + } + + protected override IBrush? ProduceValue(double progress) + { + return progress < 0.5 ? _from : _to; + } } } } From a6cfe88c61bf971ce20fc6c9dabde5b3553a049e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Jun 2021 23:40:40 +0200 Subject: [PATCH 033/184] Change ImmutableSolidColorBrush to a class. --- .../Media/Immutable/ImmutableSolidColorBrush.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs index 010184ad3b..8e93ac580e 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Immutable /// /// Fills an area with a solid color. /// - public readonly struct ImmutableSolidColorBrush : ISolidColorBrush, IEquatable + public class ImmutableSolidColorBrush : ISolidColorBrush, IEquatable { /// /// Initializes a new instance of the class. @@ -48,8 +48,9 @@ namespace Avalonia.Media.Immutable public bool Equals(ImmutableSolidColorBrush other) { - // ReSharper disable once CompareOfFloatsByEqualityOperator - return Color == other.Color && Opacity == other.Opacity; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Color.Equals(other.Color) && Opacity.Equals(other.Opacity); } public override bool Equals(object obj) @@ -67,12 +68,12 @@ namespace Avalonia.Media.Immutable public static bool operator ==(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) { - return left.Equals(right); + return Equals(left, right); } public static bool operator !=(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) { - return !left.Equals(right); + return !Equals(left, right); } /// From f9103e2c9561b614b3c94a71839c80f0ae8d4416 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Jun 2021 23:41:13 +0200 Subject: [PATCH 034/184] Parse brushes as immutable by default. --- src/Avalonia.Visuals/Media/Brush.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Visuals/Media/Brush.cs index fb03d19a4e..cf7f5f531c 100644 --- a/src/Avalonia.Visuals/Media/Brush.cs +++ b/src/Avalonia.Visuals/Media/Brush.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using Avalonia.Animation; using Avalonia.Animation.Animators; +using Avalonia.Media.Immutable; namespace Avalonia.Media { @@ -47,7 +48,7 @@ namespace Avalonia.Media if (s[0] == '#') { - return new SolidColorBrush(Color.Parse(s)); + return new ImmutableSolidColorBrush(Color.Parse(s)); } var brush = KnownColors.GetKnownBrush(s); From e742f81a7d2b1722d301132ce452a127a70e7af5 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Jun 2021 23:41:34 +0200 Subject: [PATCH 035/184] Parse brushes during compile time. --- .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 14 ++++++++++++++ .../Transformers/AvaloniaXamlIlWellKnownTypes.cs | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 059b5650cb..4592b9c8b4 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -207,6 +207,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return true; } + if (types.IBrush.IsAssignableFrom(type)) + { + if (Color.TryParse(text, out Color color)) + { + var brushTypeRef = new XamlAstClrTypeReference(node, types.ImmutableSolidColorBrush, false); + + result = new XamlAstNewClrObjectNode(node, brushTypeRef, + types.ImmutableSolidColorBrushConstructorColor, + new List { new XamlConstantNode(node, types.UInt, color.ToUint32()) }); + + return true; + } + } + result = null; return false; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index c4995b2de3..6dd3521183 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -80,7 +80,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType ColumnDefinitions { get; } public IXamlType Classes { get; } public IXamlMethod ClassesBindMethod { get; } - public IXamlProperty StyledElementClassesProperty { get; set; } + public IXamlProperty StyledElementClassesProperty { get; } + public IXamlType IBrush { get; } + public IXamlType ImmutableSolidColorBrush { get; } + public IXamlConstructor ImmutableSolidColorBrushConstructorColor { get; } public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { @@ -178,6 +181,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers .FindMethod( "BindClass", IDisposable, false, IStyledElement, cfg.WellKnownTypes.String, IBinding, cfg.WellKnownTypes.Object); + + IBrush = cfg.TypeSystem.GetType("Avalonia.Media.IBrush"); + ImmutableSolidColorBrush = cfg.TypeSystem.GetType("Avalonia.Media.Immutable.ImmutableSolidColorBrush"); + ImmutableSolidColorBrushConstructorColor = ImmutableSolidColorBrush.GetConstructor(new List { UInt }); } } From f877fe5ae149c4573540ddd960c40f0ad4f93e59 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Jun 2021 23:59:16 +0200 Subject: [PATCH 036/184] Use immutable data to limit allocations. --- src/Avalonia.Controls/ExperimentalAcrylicBorder.cs | 3 ++- src/Avalonia.Controls/Presenters/TextPresenter.cs | 9 ++++++--- src/Avalonia.Controls/TickBar.cs | 3 ++- src/Avalonia.Controls/Utils/BorderRenderHelper.cs | 5 +++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index 0a01767a07..57861163d6 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -3,6 +3,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using System; +using Avalonia.Media.Immutable; namespace Avalonia.Controls { @@ -90,7 +91,7 @@ namespace Avalonia.Controls } else { - _borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new SolidColorBrush(Material.FallbackColor), null, default); + _borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new ImmutableSolidColorBrush(Material.FallbackColor), null, default); } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 3bec46a9ac..5506ce05d6 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -6,6 +6,7 @@ using Avalonia.Metadata; using Avalonia.Threading; using Avalonia.VisualTree; using Avalonia.Layout; +using Avalonia.Media.Immutable; namespace Avalonia.Controls.Presenters { @@ -366,24 +367,26 @@ namespace Avalonia.Controls.Presenters if (caretBrush is null) { - var backgroundColor = (Background as SolidColorBrush)?.Color; + var backgroundColor = (Background as ISolidColorBrush)?.Color; if (backgroundColor.HasValue) { byte red = (byte)~(backgroundColor.Value.R); byte green = (byte)~(backgroundColor.Value.G); byte blue = (byte)~(backgroundColor.Value.B); - caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue)); + caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue)); } else + { caretBrush = Brushes.Black; + } } if (_caretBlink) { var (p1, p2) = GetCaretPoints(); context.DrawLine( - new Pen(caretBrush, 1), + new ImmutablePen(caretBrush, 1), p1, p2); } } diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs index 6ea5277a55..237bc2ce1d 100644 --- a/src/Avalonia.Controls/TickBar.cs +++ b/src/Avalonia.Controls/TickBar.cs @@ -1,6 +1,7 @@ using Avalonia.Collections; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Utilities; namespace Avalonia.Controls @@ -295,7 +296,7 @@ namespace Avalonia.Controls endPoint = pt; } - var pen = new Pen(Fill, 1.0d); + var pen = new ImmutablePen(Fill?.ToImmutable(), 1.0d); // Is it Vertical? if (Placement == TickBarPlacement.Left || Placement == TickBarPlacement.Right) diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 3128753781..f8ab58d46e 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.Utilities; @@ -114,9 +115,9 @@ namespace Avalonia.Controls.Utils var borderThickness = _borderThickness.Top; IPen pen = null; - if (borderThickness > 0) + if (borderBrush != null && borderThickness > 0) { - pen = new Pen(borderBrush, borderThickness); + pen = new ImmutablePen(borderBrush.ToImmutable(), borderThickness); } var rect = new Rect(_size); From 9b337d10de8fa415df3924c17455d5621cb2a9e9 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Wed, 9 Jun 2021 10:12:42 +0200 Subject: [PATCH 037/184] Cleanup. --- .../Animation/Transitions/BrushTransition.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs index 8d0b9089f9..cc5af1b4b1 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs @@ -22,9 +22,6 @@ namespace Avalonia.Animation if (oldSolidColorBrush != null && newSolidColorBrush != null) { - EnsureImmutable(ref oldSolidColorBrush); - EnsureImmutable(ref newSolidColorBrush); - return new AnimatorTransitionObservable( s_animator, progress, Easing, oldSolidColorBrush, newSolidColorBrush); } @@ -32,11 +29,6 @@ namespace Avalonia.Animation return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); } - private static void EnsureImmutable(ref ISolidColorBrush brush) - { - brush = (ISolidColorBrush)brush.ToImmutable(); - } - private static ISolidColorBrush? TryGetSolidColorBrush(IBrush? brush) { if (brush is null) @@ -54,7 +46,7 @@ namespace Avalonia.Animation public IncompatibleTransitionObservable(IObservable progress, Easing easing, IBrush? from, IBrush? to) : base(progress, easing) { - _from = @from; + _from = from; _to = to; } From 6f5bf5217899f4a4e67bdbc18f3ddb1128c59317 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 10 Jun 2021 13:15:58 +0200 Subject: [PATCH 038/184] Fix problems with mutable brush being passed to an immutable pen. --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 15 ++++++--------- .../Media/Immutable/ImmutablePen.cs | 3 +++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 5506ce05d6..ff63e5644f 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -361,9 +361,9 @@ namespace Avalonia.Controls.Presenters RenderInternal(context); - if (selectionStart == selectionEnd) + if (selectionStart == selectionEnd && _caretBlink) { - var caretBrush = CaretBrush; + var caretBrush = CaretBrush?.ToImmutable(); if (caretBrush is null) { @@ -382,13 +382,10 @@ namespace Avalonia.Controls.Presenters } } - if (_caretBlink) - { - var (p1, p2) = GetCaretPoints(); - context.DrawLine( - new ImmutablePen(caretBrush, 1), - p1, p2); - } + var (p1, p2) = GetCaretPoints(); + context.DrawLine( + new ImmutablePen(caretBrush, 1), + p1, p2); } } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs index 32624fbf45..3256f4b11a 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; namespace Avalonia.Media.Immutable { @@ -44,6 +45,8 @@ namespace Avalonia.Media.Immutable PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) { + Debug.Assert(!(brush is IMutableBrush)); + Brush = brush; Thickness = thickness; LineCap = lineCap; From 31d9b5b13add57f5a4b37258f2d2c0ee17dec413 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 10 Jun 2021 16:26:38 +0200 Subject: [PATCH 039/184] Fix a couple of tests that only fail in debug mode. Introduced in #6039 due to use of `Debug.Assert`. --- tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs index c2a1a5f9e4..8c25019606 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs @@ -59,9 +59,9 @@ namespace Avalonia.Visuals.UnitTests.Media } [Fact] - public void Equality_Is_Implemented_Between_Immutable_And_Mmutable_Pens() + public void Equality_Is_Implemented_Between_Immutable_And_Mutable_Pens() { - var brush = new SolidColorBrush(Colors.Red); + var brush = new ImmutableSolidColorBrush(Colors.Red); var target1 = new ImmutablePen( brush: brush, thickness: 2, @@ -83,7 +83,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Equality_Is_Implemented_Between_Mutable_And_Immutable_DashStyles() { - var brush = new SolidColorBrush(Colors.Red); + var brush = new ImmutableSolidColorBrush(Colors.Red); var target1 = new ImmutablePen( brush: brush, thickness: 2, From 16af23a878850874cb1fc858aa3ea21af77d0ecb Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 10 Jun 2021 17:48:49 +0200 Subject: [PATCH 040/184] fixes(DevTools): Issue #5882 binding Error 'Could not find a matching property accessor for 'Priority' and 'Could not find a matching property accessor for 'IsAttached' --- .../Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs | 8 +++++--- .../Diagnostics/ViewModels/ClrPropertyViewModel.cs | 6 ++++++ .../Diagnostics/ViewModels/PropertyViewModel.cs | 4 +++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 63f68501a7..e4c4ca6115 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -26,9 +26,11 @@ namespace Avalonia.Diagnostics.ViewModels public AvaloniaProperty Property { get; } public override object Key => Property; public override string Name { get; } - public bool IsAttached => Property.IsAttached; + public override bool? IsAttached => + Property.IsAttached; - public string Priority => _priority; + public override string Priority => + _priority; public override string Type => _type; @@ -69,7 +71,7 @@ namespace Avalonia.Diagnostics.ViewModels if (val != null) { RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority)); - RaiseAndSetIfChanged(ref _group, IsAttached ? "Attached Properties" : "Properties", nameof(Group)); + RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group)); } else { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index 6b2dbb7bae..65626aeea5 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -49,6 +49,12 @@ namespace Avalonia.Diagnostics.ViewModels } } + public override string Priority => + string.Empty; + + public override bool? IsAttached => + default; + // [MemberNotNull(nameof(_type))] public override void Update() { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index bfd098985a..fdbd8c1aa3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -16,7 +16,9 @@ namespace Avalonia.Diagnostics.ViewModels public abstract string Group { get; } public abstract string Type { get; } public abstract string Value { get; set; } - public abstract void Update(); + public abstract string Priority { get; } + public abstract bool? IsAttached { get; } + public abstract void Update(); protected static string ConvertToString(object? value) { From caa617fe6797dc1655c46c5462d8e8b9dc03ec2a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 10 Jun 2021 18:12:51 +0200 Subject: [PATCH 041/184] fixes(DevTools): Issue #6047 --- .../Diagnostics/ViewModels/ControlDetailsViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 1f7205eb16..3790951b0c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -18,7 +18,7 @@ namespace Avalonia.Diagnostics.ViewModels { private readonly IVisual _control; private readonly IDictionary> _propertyIndex; - private AvaloniaPropertyViewModel? _selectedProperty; + private PropertyViewModel? _selectedProperty; private bool _snapshotStyles; private bool _showInactiveStyles; private string? _styleStatus; @@ -139,7 +139,7 @@ namespace Avalonia.Diagnostics.ViewModels public ObservableCollection PseudoClasses { get; } - public AvaloniaPropertyViewModel? SelectedProperty + public PropertyViewModel? SelectedProperty { get => _selectedProperty; set => RaiseAndSetIfChanged(ref _selectedProperty, value); From 14ea7b44ed6c90ffbe3f5d58741e7aa545042e3c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 11 Jun 2021 18:46:13 +0200 Subject: [PATCH 042/184] 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 043/184] 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 044/184] 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 045/184] 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 046/184] 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 047/184] 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 048/184] 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 049/184] 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 zcmb7qc|4SF+kUGkgtBIt>{5!cHkO95CR<;oG$}6LN z${u44*(L^K_q(Uw_j$h0^Lw5@-anYneCEFH>)fvMK91wOL(NR|*^UbwKXBjxo1p>B z0{CoY{4p~D|BG%E6M>I|XbXMq1I3+!KY=etP|$180|!d+CwA@}J#awG+zItHuu?-6>tEj+_F415Af z*o>EG?NL#+*}s&6RQIV!J?N&eqZ-p7i%7^aN+8Q2Oh#e7aQjHF(Ck_(NI{=>ACdWI z@x}-flz8`u!J?b)tH0h^U#{t!=70oU+1d#xV_YzLk3*bJ7TI5$4AcqJNv3 zY!=$QBZ4*N!Nym^>b`-L&d5Gd(0j4(_h2*_8J+g)8j}pak%_|LU;`wmB2H3aeOQL$ zsJqS4DO#Ft$AnsA=Y6|&b4VV79)LHGf@11h?o@vV)yPNltwoO-w<%Ai_6vTdK7QPL zwL-6?N^hxDtiZ7e$&1vds|Td67? z@=P!n3m|J;?#-4T4PjU#85Fnb_)|6Hoj4@p+xOsz-4659>UK{c>by!idNG!l{eKj` z!MMF{-r7{Hi(o8Z0v7CTENewRV3JTH=V@q1zTiAgEcqxz3{O5Q)-$J0@ZApEr!5~* zr1W2NXMKCB_FgpC8}XHvi%E#rU;{g{Mi`-Vrf2Hu%pokxJ2Q=Z$^Km8^;qY-{9vk@ zrf?74S(1@XyGsTv{gKPF7bbdY8lwnqdgi|nAh>Ldt)^Lk9DZzD6`0fNv?e>yB)z#WGTE z%69|}dOj$g2CY<%gg(ocxZ^5o$ZL6`=P{z_an+GX*oCO>0vp*Ct*3K%QDU$Dxp*ET-;JWD*4 z==)V|XCq=GQR0Wm5~&!@n!$-es**$}ga+j+%8TY^eg~MSXs|0Wrn-_8aBVYif1{;t z$jCz1sjd5a9;$*Tq_D)D7k34YGmJDac9MV@6s2JyYg(EZqibid7X1O5OCi7aHE#r0o>>bJ3s|-8MwaVt8fTXHaISSTz%ET}8t`ty<0EeY>`2 ziS1YI(Q-x^N#vDX+n{@n_R|@d80_MMPHgOOOo@Y*$Tk<_IQ4-D$6I_+gr#Khc(u1Q zMBHB*IaYDi_fsVHE1@v=4!tw7%ZQ#iFe-%D0EyjDT6`D@l<6BqoP+rN4;%A16jRyENd2IY@7$JYguq}1AkYx4OiS9g3oT1eiHm0|88dU79)VA}?JG`*YNeC#7=TM^v zK^Fm}az_mmHl?Te#nlY$=wY-3GY1{AIZH{>0Xup)k$|!-nPp1qP$0$)Db0{u0zUx< z@E)6}nhky}*=yP?#UXxCbg{JyExqaZjH-Zh{5;#^1Wxy|_-p&YQ+tghiZqe#8Ut9{ z#V0;0Ja4enuH05wpdqPLH}flviBrSN={5hIJ~?mK%w0LBy>$Ic<3Ob&Sijb2R3^G~ zI)6Ve+ja@5-7KRdtf5w)id&9M}9yppyZ6=g(mCCclpW`_6= zwRVA<=2cnM&E9L21OP^1NmX;4$(nhR1Aebl${C- ze>VKn-op*!@hLEPB(W1=szBFSES+3S4&;bL1-y~UF1Zm9?fD43*gtM0C`WA9<+Ycx zKpj_asg(t>Dw^Gi6kJ@EoM_wn=TO`Zzj+;h|9~!5ln;z~8r+JZI7rdIIDLI=O+m$Y{cYUuOl{Nn4=`oI3Xbfz5$l5()-QB;# zUre;q)agCuJCOt38M`fU&6?+eYpv~&5wPgtQF%l z^C{q7pn+GEjxEWqBSr}k`mo8Q=l+ppSFOFh*b&mhh=+0mMc1$^NvH#GtwhMl^~M!8 z_q%}`qKk?5df?+Y*|Qi}&l=omN@7Rx@Tz7!-nUBacDXXDuDm;YNNuR3|FZBx$jPA9 z5YLu)(B)I+S~|Q1WzW(wE`oO!;qAlQ%k=S)0sHas@Z1i%Y`ZZ$$Qn zgnDHCrpLj)(+Dxul+AvPnS06Z0M*(#% zjm-@m-_-!yc*|uy;N^T1&R@z~(rl1m-mIY}EWdfL#tSi?gz}~Po!nF3*}8tdcE6at zqIm7tjvA&O4b_GfjeZi^KbL8bV(s4U>CYb&YOjcwHib6ew~M)A9oqP2CVLaO-bXQssPi{b`Hz-tA#g#_)Z@2vT>QnhCFC)$)$uP@l|^7lT7QZz)JE=@U18Sh>rkCT)HcLER; z5Di~8N{+v0(-kJMafdZ0rQs)i=(hr#!*2NDtO;>e6rlA|8LP%G^kNpq6mAsJl`WG$r%QX`)_H%XuJ zZk)xEOOy#IKf;z};7=fovB0Tn_JE#O&BM zyJMU=(g$@~dP!BjW1j*xE*S*g@Ir)7odeCGX6ro@K4Q@k<+vM;AEqhzI%jG#PZg;! zaeIRIPU;~2CNW)Sz2UlR^n|5&G-j{(PET*YGpD%^!i~8`G(!sW-wu{tU>Hf6B_uVl zTX_gx&##1Fanpq@8iR3@vVCjTDtaos3w_z@o9*DoUXo9b`Py$XwHgmlM9 zLCnMDHWanrR!I$ZT)@n)CbrLXX@oz>!2n10C)-z{+;Y_Jx$AyHoDakJvpI!J&*-dH zSgk4r1{ch{Ae*gyB5vG`zWk=@FrpV|TW+t%tUSggA=T;B6hSK#EWnlt|0{%=suzx82MR!PDQY*Cyw zY}V$M#w(KiY28JqX5iuefe-eTkoEO|jkuR%ibH>|aBT7@8fR#$#LTG>J7ZF$DAqpI zwB&L>_@B>fMJDsfwF_T=+-Dk+D0{$rUq6hfW2h?TmjhL}Od4vRNy{y=!jGk`Ip}Sl ztE<&`#()C?uoXhwr32|$&qC*Kh0=LglWXisD7~2~R#B(s2$P~dw9^VV-0_#xYfQ%` zZQ1S-0JC|Rx}S6Xd=PxucFw6v1=swcnPA;kB0cCsG)8~4cGe|vqzODpbT~@ny}5WP zyoDQe9Y;I$)ibiY?)RPmU;?$Rwy2j?1vjeK8I?%q`Ir<*hkjH35}!HTV^vlI40osgMT@!;8)q}MCd*|@}d{5M`9mMY>Wq6AYWRfN<9T8at})9> z`&rLWQu%dSe5lko0E~rGbbjMv0hrL$#()I|6u0;T#eWtI+o>>H%{y}b6NtCi6@6sv zGX#2?)oXa=?Q(Si#3+IPth*%jhOb(m=mX*!a7-_ zohwz478a5|a$QWVy~p$Qd&30cl~=IM8CJ=qV5U{)hg>7um46l)0*kW5&xQ^siRJl* z3Q7Ek$QY}*Qfy_W5e$Vzrj}+}8TbV^h_4;-pw6_PWss_id@$+d1Vy8BWgd=S`_cG} ztOYftivL;U6BPXqrYF!tr%iJ(2re(1Xxx-=jfOb%ztKsb@O#xINxu`~dI@K?GXGkP z9czez<&@tqj4=D&wk}=y(nR=KxCUZjXP9Pd}w8}GsAIN zurFf3`wcjF$-nMTJl*(4?{GYxaztmDhv^g(zJF=4!q63QOx58((0O0d**TK2Ii$4+&=czKiVi!lopD zbJvZ!tL2QFs0bVZRS?9o)<0xC-tq04#|Ckn^$$pwXH@FODb@*T{#wWAN>j6uC$_7t zoVQUNO{1>ITNpp8MreHly%d)nPSH6{L2{U9bHwde{IP8E66O*P$+K_y70_p(7kyU6 zA~by&vGWsbe#rE*>x`)LrAE7fepVqyYSkhZA8Ee902X65=v$w!rnDRh{xzFb^yY;) z-MZt`u}+a>=FeWyGmPKCY!(U{feV_zX%`A8heo6L4_f#l%pYYUpd}+k8OXg3jnMEq4ts)tAKdgqnU{I9m!gMw(LE;T`fEnd&LE=~ ze?1IZZN6gPqZ0s@_#W}Q7!Dt@y7)t&=I_-|bL%U^EPp1blSKNbv!#Q;Y3IB1X6n}4 zVC^4Xxy0Tt)Mlstn|hHO8uZz}QkwmEqoD5_AgmT&Q(jLBekCm6OkB?>tgGmo|Ho1m zTG0eNvr{rKr0#_5P-r465XN_T1C|?H*hQ_QkSV=eZElHAQtLS z*p);iV#)N~oZEO{KNQ=Q!-!Hd zNMfjy&r5PVd8s2YE-RFH;@v@a($hJ4)(rFeV(F;!05J_YSiI^y6;??HyK4dFln7)s zo-;J~b)V1)vN?nNpl*4!o?=Ci%Llqsh4n!%7J*P}!Ay9S)s#yQEzCFbgN$CGkaGlF zA`nQot-OJuamz`GSj+Zd85u`oCpmk4MrR%{7dsVeeY06y?-!#F)`)I1a#9TyFSxvZ zlZg;MV>4p@ww+l5MTYbx+p6GZD{{H-B4X0K)>@tEpGmPOH{j+(Fm6g*t!J4RPAY%# ztS?SIRO9OBv|(qP5|9@6f z&NIq>iA_{yt;OH8O$IqlxH~P;n?z9QzB%vP2@(iR>^Y80a#l(5^^3>X0wTqO5J*rD zT(*WAcqXoDR!Y$bi7Nyil6dmw`BAD8&Qa1)&WSgvzjHcOrA=tl2!jd&V)sTFmsP(^ zNy@5{G`t(Npw25{NzS|ewQR~x1<(%$?;$0C=am2SLKoT_#Z{L*QpB*zC&)mL5FB0? z<*nS1SsvcpnplC8X^kbQb>DO@%H_$pF2)gC_>neo;}wSB)>L3PE!B9-Va;+Lh@}UU zjJ*51`w@eRbSBQ2Gn?n@pdCrwG8LzE8R_*#v3DeP=k@xST&vAEBi^B1l&c1T2m-DZ zK*gn^Jnu67NK+Gk5+5P1Vn1!R(!o4>m!l&NSm3Bhg}IQn6rbEM%~lDU8M(#CTNZR> zu{3I`vtsewg*G{)1XY>(R&cRwCEN&F`A={dT)ra zSuD+*GHw){0t!OBzqK*S`cCuziYTWws2Pc_F?&+6K`vw32sF<{Ktb7TUwgn+DsGw8 z={!h{!isoCeNwVJq2%(Gi8Qd7q`abUj6N~L_Eu9{vdfK%W5+s8q~*BWbHclu2D^{$^J9>g(Hd>TCrK?){my$( zMWPzeRQdid`biWr`hxi}tL)bD*0n1&ozC{h4drQ3GB;3R`<58wR;ny!o<3XcD`&%I zNmBw!}f2o!|Nw-c5oziVVbAP)2Dg`O_r4b91C0e*yz?;uwJ0HqPY@3YqbDN)8p7* zpQLIH+RQi4PCp$Pij(rkkC3_Z$MZ*6qM*t!uiNnAlUQcZJQu!y!U!kC>A4#(fMwC@ zI8bByW0LZaeW0iVJCZWj-AsF2u+IG)I7M9|eM0@mw8HhX0)pkNP%IE#JI9UhC5aXE zNoqq?Y16|CveUKRzN&zHQmA!aZGe5`jv73~U+NG{JKZw!ipJ%6Ud>B}yNBg!I30o& zVE#4vB{#y}A*0+bBGsytaO8zolXF8O8fHN3Sx2BX^(c1D?cCs-4QFrjZg|Visw7R1 zdY|)w>{fW~u@%mzjtu#A*m?mfgqSdp+TBV-Dda=td+k87F)#6i%=adR;1u!wFX!M$ z@h{SoL_z$hfr+LyRR}Vy1y?0fr4^`K3l5(3 zpL4cMMmm-=xo#h)rpxmA12JovG_9h$OX%_D+lN-cZ0kilkgB`FBH_br>3Vm!=__H# z=#J=+^^I>!o;@F&rvy@&hh#n>PY0rJa}|5hF=!lq-Nv61Ie$3{x_3C{zX>Sc8~WW? zS6rEd{6wEGGm}?;UTRLCImM|ZX^3_R6qy8jFV&`^o54uIAVI<5F}|r$)~)l#?dh@j zx*O`>avJyoiKbI0;sS=oTHJrUL;dTDon!U7-GHRvRd~_Z4=d0pxPv1~KJlb_4#PKw zg=>K#1{^3f{p6L|Y$Mh`W?UM+hWe{a?W=g2dE-O&;6^FlV&vL zy*EoAc-kzl^pLxr&!2xno91)nENy?(Q6m!APl>#MalR=}Z@O5|DE*$BzyjRTo8s!Q zBVRj5l_Dy)d@hI@;bqxh!C|Y7QIwn&H&>No3w_fIbmgJHAn--m<1L_+49KMzozA*@ zXBK70BLs?{eGtvSWb-#JbIue zj1n59J{Vf58t7Sx^fG;$a;uPpSD%cZpJ_@d_q8-^2N?ACxWK8LG4Pez2s@;jYHGgcC@`n(U{^6t|Xnpf4} z3w_jXEFDoPkgpkNzTx_K{$MA`E+oyY+D0DT{XFUEwd{}AcVwqEY#UP3lMxH+uM!cY z?Xvj*TQ^3JQh?QJOa~!Yt}HZW4u6kLGk{lz1s10P;;u2OSSo4z)%w7)PX{nnU^HKz z|0Jb&eHz_YSFroyMkrVlhx^i0RkyC}|5~2;+%F6Gh>SR#Iw~kzc>p1O$$9pW@tw;y zU2+~1LRG#B)PbVzilNMmnURvW?REQ>SSowSUQ4CB!uGWf$ zBu-fZWCe^}>M;V>7uqaP5*%kUrcJLhN>CBU{&S1x=ZfOtMZ(SZ;qSzB$*;syIcvXx z_|vRirfDWURt5UMI(-x}{o`l8Ge{#lJjlS7z{NBME{XDVN#>SNBImWenr@%g;3(ta=5;vFF1sr@V^t65K=vF~&!E@jGFJqM0_uWB)ciVlVSX$X|7dmUE}C}BL28qsB=V^6eu4gWK;YP_FvS1aZ4?D6i4#`ZCfZ+!BHcHbw~~CH8z) zkmRhJv3Zs}&ppU61Lgaf+Ej?Yh=jx9l``=^V5?BrM?mA$vjXLBU^)rvq8_ZuSsJAV+R`ExUB zMhH+G%HvMD2!nZJT;6w_KKDmPDOV-G35fMx>dsRx+OlEP2Ni7a3KsQDAxF~GsGc9p zi<8Zj;ymmCys5wh%30qTAu#AHRT*by5%+Fb@ILF*3$6?Nvt%EL$W7p7n*)ZrCa_}d+fV-=WWOx# 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 050/184] 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 051/184] 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 6872b2a8a46cb47eb18cdc85cdff91e7e4394db2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 15 Jun 2021 11:53:40 +0200 Subject: [PATCH 052/184] Ensure we unsubscribe from PropertyChanged. --- src/Avalonia.Animation/AnimationInstance`1.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index 0e1882ce75..b9ba49e510 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -46,9 +46,6 @@ namespace Avalonia.Animation _onCompleteAction = OnComplete; _interpolator = Interpolator; _baseClock = baseClock; - control.PropertyChanged += ControlPropertyChanged; - - UpdateNeutralValue(); FetchProperties(); } @@ -82,6 +79,7 @@ namespace Avalonia.Animation // Animation may have been stopped before it has finished. ApplyFinalFill(); + _targetControl.PropertyChanged -= ControlPropertyChanged; _timerSub?.Dispose(); _clock.PlayState = PlayState.Stop; } @@ -90,6 +88,8 @@ namespace Avalonia.Animation { _clock = new Clock(_baseClock); _timerSub = _clock.Subscribe(Step); + _targetControl.PropertyChanged += ControlPropertyChanged; + UpdateNeutralValue(); } public void Step(TimeSpan frameTick) From c7da3ccdc555d18910635102c485747d45bb2bc0 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 15 Jun 2021 16:23:52 +0200 Subject: [PATCH 053/184] 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 054/184] 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 055/184] 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 9feb2376a0b2e11ca3d1ffa4eeade4fa8034f161 Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Tue, 15 Jun 2021 20:37:48 +0100 Subject: [PATCH 056/184] Squash toplevel-totalsize --- native/Avalonia.Native/src/OSX/window.mm | 15 +++++++++ .../Platform/SkiaPlatform/TopLevelImpl.cs | 2 ++ src/Avalonia.Controls/ApiCompatBaseline.txt | 4 ++- .../Offscreen/OffscreenTopLevelImpl.cs | 10 ++++++ .../Platform/ITopLevelImpl.cs | 5 +++ src/Avalonia.Controls/TopLevel.cs | 19 +++++++++++- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 1 + src/Avalonia.Headless/HeadlessWindowImpl.cs | 1 + src/Avalonia.Native/WindowImplBase.cs | 14 +++++++++ src/Avalonia.Native/avn.idl | 1 + src/Avalonia.X11/X11Structs.cs | 10 ++++++ src/Avalonia.X11/X11Window.cs | 31 +++++++++++++++++++ .../FramebufferToplevelImpl.cs | 1 + .../Wpf/WpfTopLevelImpl.cs | 1 + .../Interop/UnmanagedMethods.cs | 23 ++++++++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 10 ++++++ src/iOS/Avalonia.iOS/AvaloniaView.cs | 1 + .../MockWindowingPlatform.cs | 1 + 18 files changed, 148 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 870345e543..6bd09fe08b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -202,6 +202,21 @@ public: } } + virtual HRESULT GetTotalSize(AvnSize* ret) override + { + @autoreleasepool + { + if(ret == nullptr) + return E_POINTER; + + auto frame = [Window frame]; + ret->Width = frame.size.width; + ret->Height = frame.size.height; + + return S_OK; + } + } + virtual HRESULT GetScaling (double* ret) override { @autoreleasepool diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 60b772a183..8b732a92da 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -55,6 +55,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform public virtual Size ClientSize => Size.ToSize(RenderScaling); + public Size TotalSize => ClientSize; + public IMouseDevice MouseDevice { get; } = new MouseDevice(); public Action Closed { get; set; } diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index a79b3b4d7b..166a005e4d 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -11,4 +11,6 @@ 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 +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.ITopLevelImpl.TotalSize' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.ITopLevelImpl.TotalSize.get()' is present in the implementation but not in the contract. +Total Issues: 14 diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index ca0e9d48b8..cb0c5d94e5 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -35,6 +35,16 @@ namespace Avalonia.Controls.Embedding.Offscreen } } + public Size TotalSize + { + get { return _clientSize; } + set + { + _clientSize = value; + Resized?.Invoke(value); + } + } + public double RenderScaling { get { return _scaling; } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 09f38042a1..546184f8a6 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -22,6 +22,11 @@ namespace Avalonia.Platform /// Size ClientSize { get; } + /// + /// Gets the total size of the toplevel, excluding shadows. + /// + Size TotalSize { get; } + /// /// Gets the scaling factor for the toplevel. This is used for rendering. /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 7a92836ddf..e2d9e7e697 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -42,6 +42,12 @@ namespace Avalonia.Controls public static readonly DirectProperty ClientSizeProperty = AvaloniaProperty.RegisterDirect(nameof(ClientSize), o => o.ClientSize); + /// + /// Defines the property. + /// + public static readonly DirectProperty TotalSizeProperty = + AvaloniaProperty.RegisterDirect(nameof(TotalSize), o => o.TotalSize); + /// /// Defines the property. /// @@ -74,6 +80,7 @@ namespace Avalonia.Controls private readonly IPlatformRenderInterface _renderInterface; private readonly IGlobalStyles _globalStyles; private Size _clientSize; + private Size _totalSize; private WindowTransparencyLevel _actualTransparencyLevel; private ILayoutManager _layoutManager; private Border _transparencyFallbackBorder; @@ -84,6 +91,7 @@ namespace Avalonia.Controls static TopLevel() { AffectsMeasure(ClientSizeProperty); + AffectsMeasure(TotalSizeProperty); TransparencyLevelHintProperty.Changed.AddClassHandler( (tl, e) => @@ -194,9 +202,18 @@ namespace Avalonia.Controls public Size ClientSize { get { return _clientSize; } - protected set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); } + protected set + { + SetAndRaise(ClientSizeProperty, ref _clientSize, value); + SetAndRaise(TotalSizeProperty, ref _totalSize, PlatformImpl.TotalSize); + } } + /// + /// Gets or sets the total size of the window. + /// + public Size TotalSize => _totalSize; + /// /// Gets or sets the that the TopLevel should use when possible. /// diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index eedfc52d9d..91bc93d897 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -21,6 +21,7 @@ namespace Avalonia.DesignerSupport.Remote public IPlatformHandle Handle { get; } public Size MaxAutoSizeHint { get; } public Size ClientSize { get; } + public Size TotalSize => ClientSize; public double RenderScaling { get; } = 1.0; public double DesktopScaling => 1.0; public IEnumerable Surfaces { get; } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index af522f3e36..74f8c95ade 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -41,6 +41,7 @@ namespace Avalonia.Headless } public Size ClientSize { get; set; } + public Size TotalSize => ClientSize; public double RenderScaling { get; } = 1; public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index f716464d14..18efd1a519 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -104,6 +104,20 @@ namespace Avalonia.Native } } + public Size TotalSize + { + get + { + if (_native != null) + { + var s = _native.TotalSize; + return new Size(s.Width, s.Height); + } + + return default; + } + } + public IEnumerable Surfaces => new[] { (_gpu ? _glSurface : (object)null), this diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index adcbeb2d3a..aeb456c3ea 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -435,6 +435,7 @@ interface IAvnWindowBase : IUnknown HRESULT Close(); HRESULT Activate(); HRESULT GetClientSize(AvnSize*ret); + HRESULT GetTotalSize(AvnSize*ret); HRESULT GetScaling(double*ret); HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize); HRESULT Resize(double width, double height); diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index b1006b43ee..604fbf2b24 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -1740,6 +1740,16 @@ namespace Avalonia.X11 { public short W; public short H; } + + [StructLayout (LayoutKind.Sequential)] + [Serializable] + internal struct XFrameExtents + { + public int Left; + public int Right; + public int Top; + public int Bottom; + } [StructLayout (LayoutKind.Sequential)] diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 5ac4c4c9d0..c0a38eee95 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -297,6 +297,30 @@ namespace Avalonia.X11 public Size ClientSize => new Size(_realSize.Width / RenderScaling, _realSize.Height / RenderScaling); + public Size TotalSize + { + get + { + XGetWindowProperty(_x11.Display, _handle, _x11.Atoms._NET_FRAME_EXTENTS, IntPtr.Zero, + new IntPtr(4), false, (IntPtr)Atom.AnyPropertyType, out var _, + out var _, out var nitems, out var _, out var prop); + + if (nitems.ToInt64() != 4) + { + // Window hasn't been mapped by the WM yet, so can't get the extents. + return ClientSize; + } + + var data = (IntPtr*)prop.ToPointer(); + var extents = new Thickness(data[0].ToInt32(), data[2].ToInt32(), data[1].ToInt32(), data[3].ToInt32()); + XFree(prop); + + return new Size( + (_realSize.Width + extents.Left + extents.Right) / RenderScaling, + (_realSize.Height + extents.Top + extents.Bottom) / RenderScaling); + } + } + public double RenderScaling { get @@ -589,6 +613,13 @@ namespace Avalonia.X11 private void OnPropertyChange(IntPtr atom, bool hasValue) { + if (atom == _x11.Atoms._NET_FRAME_EXTENTS) + { + // Occurs once the window has been mapped, which is the earliest the extents + // can be retrieved, so invoke event to force update of TopLevel.TotalSize. + Resized.Invoke(ClientSize); + } + if (atom == _x11.Atoms._NET_WM_STATE) { WindowState state = WindowState.Normal; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 4bbb58e53e..7f231aee17 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -62,6 +62,7 @@ namespace Avalonia.LinuxFramebuffer } public Size ClientSize => ScaledSize; + public Size TotalSize => ClientSize; public IMouseDevice MouseDevice => new MouseDevice(); public IPopupImpl CreatePopup() => null; diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index fe1d625efb..eedf99b33a 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -100,6 +100,7 @@ namespace Avalonia.Win32.Interop.Wpf } Size ITopLevelImpl.ClientSize => _finalSize; + Size ITopLevelImpl.TotalSize => _finalSize; IMouseDevice ITopLevelImpl.MouseDevice => _mouse; double ITopLevelImpl.RenderScaling => PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M11 ?? 1; diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index c137926e4c..ad409810b8 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -745,6 +745,26 @@ namespace Avalonia.Win32.Interop WM_DISPATCH_WORK_ITEM = WM_USER, } + public enum DwmWindowAttribute : uint + { + DWMWA_NCRENDERING_ENABLED = 1, + DWMWA_NCRENDERING_POLICY, + DWMWA_TRANSITIONS_FORCEDISABLED, + DWMWA_ALLOW_NCPAINT, + DWMWA_CAPTION_BUTTON_BOUNDS, + DWMWA_NONCLIENT_RTL_LAYOUT, + DWMWA_FORCE_ICONIC_REPRESENTATION, + DWMWA_FLIP3D_POLICY, + DWMWA_EXTENDED_FRAME_BOUNDS, + DWMWA_HAS_ICONIC_BITMAP, + DWMWA_DISALLOW_PEEK, + DWMWA_EXCLUDED_FROM_PEEK, + DWMWA_CLOAK, + DWMWA_CLOAKED, + DWMWA_FREEZE_REPRESENTATION, + DWMWA_LAST + }; + public enum MapVirtualKeyMapTypes : uint { MAPVK_VK_TO_VSC = 0x00, @@ -1388,6 +1408,9 @@ namespace Avalonia.Win32.Interop [DllImport("dwmapi.dll")] public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); + [DllImport("dwmapi.dll")] + public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out RECT pvAttribute, int cbAttribute); + [DllImport("dwmapi.dll")] public static extern int DwmIsCompositionEnabled(out bool enabled); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 3a3342fd14..bb3f78715d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -213,6 +213,16 @@ namespace Avalonia.Win32 } } + public Size TotalSize + { + get + { + DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf(typeof(RECT))); + + return new Size(rect.Width, rect.Height) / RenderScaling; + } + } + public IScreenImpl Screen { get; } public IPlatformHandle Handle { get; private set; } diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 36a70ea410..ecd2a46b12 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -91,6 +91,7 @@ namespace Avalonia.iOS } public Size ClientSize => new Size(_view.Bounds.Width, _view.Bounds.Height); + public Size TotalSize => ClientSize; public double RenderScaling => _view.ContentScaleFactor; public IEnumerable Surfaces { get; set; } public Action Input { get; set; } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 8a24a8366f..51c202f6bd 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -29,6 +29,7 @@ namespace Avalonia.UnitTests windowImpl.SetupAllProperties(); windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); + windowImpl.Setup(x => x.TotalSize).Returns(() => clientSize); windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); 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 057/184] 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 058/184] 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 d77d1fd5dab36976232bfd9a565a9c2d7cc956cf Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Tue, 15 Jun 2021 21:51:43 +0100 Subject: [PATCH 059/184] Rename to FrameSize --- native/Avalonia.Native/src/OSX/window.mm | 2 +- .../Platform/SkiaPlatform/TopLevelImpl.cs | 2 +- src/Avalonia.Controls/ApiCompatBaseline.txt | 4 +-- .../Offscreen/OffscreenTopLevelImpl.cs | 10 +------- .../Platform/ITopLevelImpl.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 25 +++++++++++-------- src/Avalonia.Controls/Window.cs | 1 + src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Headless/HeadlessWindowImpl.cs | 2 +- src/Avalonia.Native/WindowImplBase.cs | 4 +-- src/Avalonia.Native/avn.idl | 2 +- src/Avalonia.X11/X11Window.cs | 4 +-- .../FramebufferToplevelImpl.cs | 2 +- .../Wpf/WpfTopLevelImpl.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- src/iOS/Avalonia.iOS/AvaloniaView.cs | 2 +- .../MockWindowingPlatform.cs | 2 +- 17 files changed, 33 insertions(+), 37 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 6bd09fe08b..76eca330a9 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -202,7 +202,7 @@ public: } } - virtual HRESULT GetTotalSize(AvnSize* ret) override + virtual HRESULT GetFrameSize(AvnSize* ret) override { @autoreleasepool { diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 8b732a92da..1e1e7066d4 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -55,7 +55,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public virtual Size ClientSize => Size.ToSize(RenderScaling); - public Size TotalSize => ClientSize; + public Size FrameSize => ClientSize; public IMouseDevice MouseDevice { get; } = new MouseDevice(); diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 166a005e4d..f81506252a 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -11,6 +11,6 @@ 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. -InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.ITopLevelImpl.TotalSize' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.ITopLevelImpl.TotalSize.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. Total Issues: 14 diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index cb0c5d94e5..9242264470 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -35,15 +35,7 @@ namespace Avalonia.Controls.Embedding.Offscreen } } - public Size TotalSize - { - get { return _clientSize; } - set - { - _clientSize = value; - Resized?.Invoke(value); - } - } + public Size FrameSize => _clientSize; public double RenderScaling { diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 546184f8a6..d9fb22422a 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -25,7 +25,7 @@ namespace Avalonia.Platform /// /// Gets the total size of the toplevel, excluding shadows. /// - Size TotalSize { get; } + Size FrameSize { get; } /// /// Gets the scaling factor for the toplevel. This is used for rendering. diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index e2d9e7e697..65bd2b8d2f 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -43,10 +43,10 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect(nameof(ClientSize), o => o.ClientSize); /// - /// Defines the property. + /// Defines the property. /// - public static readonly DirectProperty TotalSizeProperty = - AvaloniaProperty.RegisterDirect(nameof(TotalSize), o => o.TotalSize); + public static readonly DirectProperty FrameSizeProperty = + AvaloniaProperty.RegisterDirect(nameof(FrameSize), o => o.FrameSize); /// /// Defines the property. @@ -80,7 +80,7 @@ namespace Avalonia.Controls private readonly IPlatformRenderInterface _renderInterface; private readonly IGlobalStyles _globalStyles; private Size _clientSize; - private Size _totalSize; + private Size _frameSize; private WindowTransparencyLevel _actualTransparencyLevel; private ILayoutManager _layoutManager; private Border _transparencyFallbackBorder; @@ -91,7 +91,6 @@ namespace Avalonia.Controls static TopLevel() { AffectsMeasure(ClientSizeProperty); - AffectsMeasure(TotalSizeProperty); TransparencyLevelHintProperty.Changed.AddClassHandler( (tl, e) => @@ -202,17 +201,13 @@ namespace Avalonia.Controls public Size ClientSize { get { return _clientSize; } - protected set - { - SetAndRaise(ClientSizeProperty, ref _clientSize, value); - SetAndRaise(TotalSizeProperty, ref _totalSize, PlatformImpl.TotalSize); - } + protected set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); } } /// /// Gets or sets the total size of the window. /// - public Size TotalSize => _totalSize; + public Size FrameSize => _frameSize; /// /// Gets or sets the that the TopLevel should use when possible. @@ -462,6 +457,14 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnClosed(EventArgs e) => Closed?.Invoke(this, e); + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == ClientSizeProperty) + { + SetAndRaise(FrameSizeProperty, ref _frameSize, PlatformImpl.FrameSize); + } + } + /// /// Tries to get a service from an , logging a /// warning if not found. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 700c3d9bad..cc8e27c3ec 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -950,6 +950,7 @@ namespace Avalonia.Controls protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { + base.OnPropertyChanged(change); if (change.Property == SystemDecorationsProperty) { var typedNewValue = change.NewValue.GetValueOrDefault(); diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 91bc93d897..aaaaedd2a6 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -21,7 +21,7 @@ namespace Avalonia.DesignerSupport.Remote public IPlatformHandle Handle { get; } public Size MaxAutoSizeHint { get; } public Size ClientSize { get; } - public Size TotalSize => ClientSize; + public Size FrameSize => ClientSize; public double RenderScaling { get; } = 1.0; public double DesktopScaling => 1.0; public IEnumerable Surfaces { get; } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 74f8c95ade..b1d53bc599 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -41,7 +41,7 @@ namespace Avalonia.Headless } public Size ClientSize { get; set; } - public Size TotalSize => ClientSize; + public Size FrameSize => ClientSize; public double RenderScaling { get; } = 1; public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 18efd1a519..f37c72cab4 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -104,13 +104,13 @@ namespace Avalonia.Native } } - public Size TotalSize + public Size FrameSize { get { if (_native != null) { - var s = _native.TotalSize; + var s = _native.FrameSize; return new Size(s.Width, s.Height); } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index aeb456c3ea..89e20463d8 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -435,7 +435,7 @@ interface IAvnWindowBase : IUnknown HRESULT Close(); HRESULT Activate(); HRESULT GetClientSize(AvnSize*ret); - HRESULT GetTotalSize(AvnSize*ret); + HRESULT GetFrameSize(AvnSize*ret); HRESULT GetScaling(double*ret); HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize); HRESULT Resize(double width, double height); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index c0a38eee95..8493238b68 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -297,7 +297,7 @@ namespace Avalonia.X11 public Size ClientSize => new Size(_realSize.Width / RenderScaling, _realSize.Height / RenderScaling); - public Size TotalSize + public Size FrameSize { get { @@ -616,7 +616,7 @@ namespace Avalonia.X11 if (atom == _x11.Atoms._NET_FRAME_EXTENTS) { // Occurs once the window has been mapped, which is the earliest the extents - // can be retrieved, so invoke event to force update of TopLevel.TotalSize. + // can be retrieved, so invoke event to force update of TopLevel.FrameSize. Resized.Invoke(ClientSize); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 7f231aee17..19607ec47e 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -62,7 +62,7 @@ namespace Avalonia.LinuxFramebuffer } public Size ClientSize => ScaledSize; - public Size TotalSize => ClientSize; + public Size FrameSize => ClientSize; public IMouseDevice MouseDevice => new MouseDevice(); public IPopupImpl CreatePopup() => null; diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index eedf99b33a..f78328e36c 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -100,7 +100,7 @@ namespace Avalonia.Win32.Interop.Wpf } Size ITopLevelImpl.ClientSize => _finalSize; - Size ITopLevelImpl.TotalSize => _finalSize; + Size ITopLevelImpl.FrameSize => _finalSize; IMouseDevice ITopLevelImpl.MouseDevice => _mouse; double ITopLevelImpl.RenderScaling => PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M11 ?? 1; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index bb3f78715d..6e105d2152 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -213,7 +213,7 @@ namespace Avalonia.Win32 } } - public Size TotalSize + public Size FrameSize { get { diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index ecd2a46b12..f11cd1983b 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -91,7 +91,7 @@ namespace Avalonia.iOS } public Size ClientSize => new Size(_view.Bounds.Width, _view.Bounds.Height); - public Size TotalSize => ClientSize; + public Size FrameSize => ClientSize; public double RenderScaling => _view.ContentScaleFactor; public IEnumerable Surfaces { get; set; } public Action Input { get; set; } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 51c202f6bd..8db8f16784 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -29,7 +29,7 @@ namespace Avalonia.UnitTests windowImpl.SetupAllProperties(); windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); - windowImpl.Setup(x => x.TotalSize).Returns(() => clientSize); + windowImpl.Setup(x => x.FrameSize).Returns(() => clientSize); windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); From e65241992797ac596e782a56eb02ffd3ac8ada48 Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Tue, 15 Jun 2021 22:09:35 +0100 Subject: [PATCH 060/184] Fix regression --- src/Avalonia.Controls/TopLevel.cs | 16 +++++++--------- src/Avalonia.Controls/Window.cs | 1 - src/Avalonia.Controls/WindowBase.cs | 1 + 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 65bd2b8d2f..bbd551ace3 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -168,6 +168,7 @@ namespace Avalonia.Controls styler?.ApplyStyles(this); ClientSize = impl.ClientSize; + FrameSize = impl.FrameSize; this.GetObservable(PointerOverElementProperty) .Select( @@ -207,7 +208,11 @@ namespace Avalonia.Controls /// /// Gets or sets the total size of the window. /// - public Size FrameSize => _frameSize; + public Size FrameSize + { + get { return _frameSize; } + protected set { SetAndRaise(FrameSizeProperty, ref _frameSize, value); } + } /// /// Gets or sets the that the TopLevel should use when possible. @@ -378,6 +383,7 @@ namespace Avalonia.Controls protected virtual void HandleResized(Size clientSize) { ClientSize = clientSize; + FrameSize = PlatformImpl.FrameSize; Width = clientSize.Width; Height = clientSize.Height; LayoutManager.ExecuteLayoutPass(); @@ -457,14 +463,6 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnClosed(EventArgs e) => Closed?.Invoke(this, e); - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - if (change.Property == ClientSizeProperty) - { - SetAndRaise(FrameSizeProperty, ref _frameSize, PlatformImpl.FrameSize); - } - } - /// /// Tries to get a service from an , logging a /// warning if not found. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index cc8e27c3ec..700c3d9bad 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -950,7 +950,6 @@ namespace Avalonia.Controls protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(change); if (change.Property == SystemDecorationsProperty) { var typedNewValue = change.NewValue.GetValueOrDefault(); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index cdcb499e98..2b31cef8bd 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -222,6 +222,7 @@ namespace Avalonia.Controls protected override void HandleResized(Size clientSize) { ClientSize = clientSize; + FrameSize = PlatformImpl.FrameSize; LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); } 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 061/184] 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 062/184] 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 063/184] 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 3c911839005ab0b56561aae39a303353bf812397 Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Wed, 16 Jun 2021 13:39:23 +0100 Subject: [PATCH 064/184] Fix when dwm disabled --- src/Windows/Avalonia.Win32/WindowImpl.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 6e105d2152..645cb5fb99 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -217,8 +217,13 @@ namespace Avalonia.Win32 { get { - DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf(typeof(RECT))); + if (DwmIsCompositionEnabled(out var compositionEnabled) != 0 || !compositionEnabled) + { + GetWindowRect(_hwnd, out var rcWindow); + return new Size(rcWindow.Width, rcWindow.Height) / RenderScaling; + } + DwmGetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_EXTENDED_FRAME_BOUNDS, out var rect, Marshal.SizeOf(typeof(RECT))); return new Size(rect.Width, rect.Height) / RenderScaling; } } From d5f1365de911adc52bebc1433f05b49df8b49e0c Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Wed, 16 Jun 2021 15:21:28 +0100 Subject: [PATCH 065/184] Revert unused addition --- src/Avalonia.X11/X11Structs.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index 604fbf2b24..b1006b43ee 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -1740,16 +1740,6 @@ namespace Avalonia.X11 { public short W; public short H; } - - [StructLayout (LayoutKind.Sequential)] - [Serializable] - internal struct XFrameExtents - { - public int Left; - public int Right; - public int Top; - public int Bottom; - } [StructLayout (LayoutKind.Sequential)] From f7cc89c86bba426b7449c3bab53d0976390ead94 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 16 Jun 2021 16:01:36 +0100 Subject: [PATCH 066/184] 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 038805bb26c74f8896093fb0204c73082de34268 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Jun 2021 17:23:48 +0200 Subject: [PATCH 067/184] Cache property changed delegate. --- src/Avalonia.Animation/AnimationInstance`1.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index b9ba49e510..cf79640150 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -37,6 +37,7 @@ namespace Avalonia.Animation private IDisposable _timerSub; private readonly IClock _baseClock; private IClock _clock; + private EventHandler _propertyChangedDelegate; public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator) { @@ -79,7 +80,7 @@ namespace Avalonia.Animation // Animation may have been stopped before it has finished. ApplyFinalFill(); - _targetControl.PropertyChanged -= ControlPropertyChanged; + _targetControl.PropertyChanged -= _propertyChangedDelegate; _timerSub?.Dispose(); _clock.PlayState = PlayState.Stop; } @@ -88,7 +89,8 @@ namespace Avalonia.Animation { _clock = new Clock(_baseClock); _timerSub = _clock.Subscribe(Step); - _targetControl.PropertyChanged += ControlPropertyChanged; + _propertyChangedDelegate ??= ControlPropertyChanged; + _targetControl.PropertyChanged += _propertyChangedDelegate; UpdateNeutralValue(); } 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 068/184] 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 069/184] 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 070/184] 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 071/184] 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)> From bccbf0570c9891d8b46db6105e394dcf630c86a7 Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Thu, 17 Jun 2021 11:06:32 +0100 Subject: [PATCH 072/184] Make FrameSize nullable --- .../Platform/SkiaPlatform/TopLevelImpl.cs | 2 +- src/Avalonia.Controls/ApiCompatBaseline.txt | 4 ++-- .../Embedding/Offscreen/OffscreenTopLevelImpl.cs | 2 +- src/Avalonia.Controls/Platform/ITopLevelImpl.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 8 ++++---- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Headless/HeadlessWindowImpl.cs | 2 +- src/Avalonia.Native/WindowImplBase.cs | 2 +- src/Avalonia.X11/X11Window.cs | 4 ++-- .../Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs | 2 +- src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- src/iOS/Avalonia.iOS/AvaloniaView.cs | 2 +- tests/Avalonia.UnitTests/MockWindowingPlatform.cs | 1 - 14 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 1e1e7066d4..a72742580c 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -55,7 +55,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public virtual Size ClientSize => Size.ToSize(RenderScaling); - public Size FrameSize => ClientSize; + public Size? FrameSize => null; public IMouseDevice MouseDevice { get; } = new MouseDevice(); diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index f81506252a..443b085aa4 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -11,6 +11,6 @@ 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. -InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. Total Issues: 14 diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 9242264470..83470f161d 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls.Embedding.Offscreen } } - public Size FrameSize => _clientSize; + public Size? FrameSize => null; public double RenderScaling { diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index d9fb22422a..6e53233898 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -25,7 +25,7 @@ namespace Avalonia.Platform /// /// Gets the total size of the toplevel, excluding shadows. /// - Size FrameSize { get; } + Size? FrameSize { get; } /// /// Gets the scaling factor for the toplevel. This is used for rendering. diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index bbd551ace3..7028dca769 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -45,8 +45,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty FrameSizeProperty = - AvaloniaProperty.RegisterDirect(nameof(FrameSize), o => o.FrameSize); + public static readonly DirectProperty FrameSizeProperty = + AvaloniaProperty.RegisterDirect(nameof(FrameSize), o => o.FrameSize); /// /// Defines the property. @@ -80,7 +80,7 @@ namespace Avalonia.Controls private readonly IPlatformRenderInterface _renderInterface; private readonly IGlobalStyles _globalStyles; private Size _clientSize; - private Size _frameSize; + private Size? _frameSize; private WindowTransparencyLevel _actualTransparencyLevel; private ILayoutManager _layoutManager; private Border _transparencyFallbackBorder; @@ -208,7 +208,7 @@ namespace Avalonia.Controls /// /// Gets or sets the total size of the window. /// - public Size FrameSize + public Size? FrameSize { get { return _frameSize; } protected set { SetAndRaise(FrameSizeProperty, ref _frameSize, value); } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index aaaaedd2a6..c8203686f9 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -21,7 +21,7 @@ namespace Avalonia.DesignerSupport.Remote public IPlatformHandle Handle { get; } public Size MaxAutoSizeHint { get; } public Size ClientSize { get; } - public Size FrameSize => ClientSize; + public Size? FrameSize => null; public double RenderScaling { get; } = 1.0; public double DesktopScaling => 1.0; public IEnumerable Surfaces { get; } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index b1d53bc599..7f4b9face4 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -41,7 +41,7 @@ namespace Avalonia.Headless } public Size ClientSize { get; set; } - public Size FrameSize => ClientSize; + public Size? FrameSize => null; public double RenderScaling { get; } = 1; public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index f37c72cab4..ced9cea3a8 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -104,7 +104,7 @@ namespace Avalonia.Native } } - public Size FrameSize + public Size? FrameSize { get { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 8493238b68..3425da5deb 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -297,7 +297,7 @@ namespace Avalonia.X11 public Size ClientSize => new Size(_realSize.Width / RenderScaling, _realSize.Height / RenderScaling); - public Size FrameSize + public Size? FrameSize { get { @@ -308,7 +308,7 @@ namespace Avalonia.X11 if (nitems.ToInt64() != 4) { // Window hasn't been mapped by the WM yet, so can't get the extents. - return ClientSize; + return null; } var data = (IntPtr*)prop.ToPointer(); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 19607ec47e..73b1b87ce9 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -62,7 +62,7 @@ namespace Avalonia.LinuxFramebuffer } public Size ClientSize => ScaledSize; - public Size FrameSize => ClientSize; + public Size? FrameSize => null; public IMouseDevice MouseDevice => new MouseDevice(); public IPopupImpl CreatePopup() => null; diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index f78328e36c..73e46b9e13 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -100,7 +100,7 @@ namespace Avalonia.Win32.Interop.Wpf } Size ITopLevelImpl.ClientSize => _finalSize; - Size ITopLevelImpl.FrameSize => _finalSize; + Size? ITopLevelImpl.FrameSize => null; IMouseDevice ITopLevelImpl.MouseDevice => _mouse; double ITopLevelImpl.RenderScaling => PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M11 ?? 1; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 645cb5fb99..646a6f5739 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -213,7 +213,7 @@ namespace Avalonia.Win32 } } - public Size FrameSize + public Size? FrameSize { get { diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index f11cd1983b..0371a7759a 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -91,7 +91,7 @@ namespace Avalonia.iOS } public Size ClientSize => new Size(_view.Bounds.Width, _view.Bounds.Height); - public Size FrameSize => ClientSize; + public Size? FrameSize => null; public double RenderScaling => _view.ContentScaleFactor; public IEnumerable Surfaces { get; set; } public Action Input { get; set; } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 8db8f16784..8a24a8366f 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -29,7 +29,6 @@ namespace Avalonia.UnitTests windowImpl.SetupAllProperties(); windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); - windowImpl.Setup(x => x.FrameSize).Returns(() => clientSize); windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); From f10f54682161bdcfc87ea26d823924691bf90885 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 17 Jun 2021 12:49:51 +0100 Subject: [PATCH 073/184] scale flyout transient area. --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 4725d17d65..ea47d15914 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -252,7 +252,9 @@ namespace Avalonia.Controls.Primitives if (Popup?.Host is PopupRoot root) { // Get the popup root bounds and convert to screen coordinates - var tmp = root.Bounds.Inflate(100); + var topLevel = root.Parent as TopLevel; + + var tmp = root.Bounds.Inflate(topLevel.PointToClient(new PixelPoint(100, 100)).X); var scPt = root.PointToScreen(tmp.TopLeft); enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height); } From b80ee8672954999a2c52212bff0deb5f458078eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 17 Jun 2021 12:53:06 +0100 Subject: [PATCH 074/184] no need for toplevel. --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index ea47d15914..b3b5b5238e 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -252,9 +252,7 @@ namespace Avalonia.Controls.Primitives if (Popup?.Host is PopupRoot root) { // Get the popup root bounds and convert to screen coordinates - var topLevel = root.Parent as TopLevel; - - var tmp = root.Bounds.Inflate(topLevel.PointToClient(new PixelPoint(100, 100)).X); + var tmp = root.Bounds.Inflate(root.PointToClient(new PixelPoint(100, 100)).X); var scPt = root.PointToScreen(tmp.TopLeft); enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height); } From 45299c15059732404ef07a88e0d2987af0186433 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 17 Jun 2021 13:04:28 +0100 Subject: [PATCH 075/184] use renderscaling. --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index b3b5b5238e..73f2329513 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -4,6 +4,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.Rendering; #nullable enable @@ -252,7 +253,8 @@ namespace Avalonia.Controls.Primitives if (Popup?.Host is PopupRoot root) { // Get the popup root bounds and convert to screen coordinates - var tmp = root.Bounds.Inflate(root.PointToClient(new PixelPoint(100, 100)).X); + + var tmp = root.Bounds.Inflate(100 * (root as IRenderRoot).RenderScaling); var scPt = root.PointToScreen(tmp.TopLeft); enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height); } From b78d2a0e06808c32ef1573d7dda249fc4605f342 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 17 Jun 2021 14:44:43 +0100 Subject: [PATCH 076/184] use Pixel api types when working in screen coordinates. --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 32 +++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 73f2329513..e4b68c62fd 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -52,8 +52,9 @@ namespace Avalonia.Controls.Primitives private bool _isOpen; private Control? _target; private FlyoutShowMode _showMode = FlyoutShowMode.Standard; - private Rect? enlargedPopupRect; - private IDisposable? transientDisposable; + private Rect? _enlargedPopupRect; + private PixelRect? _enlargePopupRectScreenPixelRect; + private IDisposable? _transientDisposable; protected Popup? Popup { get; private set; } @@ -164,8 +165,10 @@ namespace Avalonia.Controls.Primitives Popup.IsOpen = false; // Ensure this isn't active - transientDisposable?.Dispose(); - transientDisposable = null; + _transientDisposable?.Dispose(); + _transientDisposable = null; + _enlargedPopupRect = null; + _enlargePopupRectScreenPixelRect = null; OnClosed(); } @@ -231,7 +234,7 @@ namespace Avalonia.Controls.Primitives } else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway) { - transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss); + _transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss); } } @@ -247,21 +250,20 @@ namespace Avalonia.Controls.Primitives // For windowed popups, enlargedPopupRect is in screen coordinates, // for overlay popups, its in OverlayLayer coordinates - if (enlargedPopupRect == null) + if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null) { // Only do this once when the Flyout opens & cache the result if (Popup?.Host is PopupRoot root) { // Get the popup root bounds and convert to screen coordinates - var tmp = root.Bounds.Inflate(100 * (root as IRenderRoot).RenderScaling); - var scPt = root.PointToScreen(tmp.TopLeft); - enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height); + var tmp = root.Bounds.Inflate(100); + _enlargePopupRectScreenPixelRect = new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight)); } else if (Popup?.Host is OverlayPopupHost host) { // Overlay popups are in OverlayLayer coordinates, just use that - enlargedPopupRect = host.Bounds.Inflate(100); + _enlargedPopupRect = host.Bounds.Inflate(100); } return; @@ -275,24 +277,18 @@ namespace Avalonia.Controls.Primitives // window will not close this (as pointer events stop), which // does match UWP var pt = pArgs.Root.PointToScreen(pArgs.Position); - if (!enlargedPopupRect?.Contains(new Point(pt.X, pt.Y)) ?? false) + if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false) { HideCore(false); - enlargedPopupRect = null; - transientDisposable?.Dispose(); - transientDisposable = null; } } else if (Popup?.Host is OverlayPopupHost) { // Same as above here, but just different coordinate space // so we don't need to translate - if (!enlargedPopupRect?.Contains(pArgs.Position) ?? false) + if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false) { HideCore(false); - enlargedPopupRect = null; - transientDisposable?.Dispose(); - transientDisposable = null; } } } From b37ddadf811f97dd6ebaf181460b349d1c49e5f1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Jun 2021 16:31:57 +0200 Subject: [PATCH 077/184] Implement measure pass in AdornerLayer. Previously the `AdornerLayer` was falling back to the base class implementation of `MeasureOverride`, in `Canvas`. This meant that at times, adorner controls were being measured with a larger constraint than the final size given to them. `Grid` doesn't like it when this happens which causes problems with various controls. Add a measure pass to `AdornerLayer` which passes the adorned control's bounds as the constraint (which is the value that the arrange pass will use). --- .../Primitives/AdornerLayer.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 9834bf3d3b..a397608aba 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Specialized; using System.Linq; +using System.Resources; using Avalonia.Media; using Avalonia.Rendering; using Avalonia.Utilities; @@ -45,10 +46,27 @@ namespace Avalonia.Controls.Primitives ?.AdornerLayer; } - protected override Size ArrangeOverride(Size finalSize) + protected override Size MeasureOverride(Size availableSize) { - var parent = Parent; + foreach (var child in Children) + { + var info = child.GetValue(s_adornedElementInfoProperty); + + if (info != null && info.Bounds.HasValue) + { + child.Measure(info.Bounds.Value.Bounds.Size); + } + else + { + child.Measure(availableSize); + } + } + return default; + } + + protected override Size ArrangeOverride(Size finalSize) + { foreach (var child in Children) { var info = child.GetValue(s_adornedElementInfoProperty); From eb3933b636bea07a7a06e1e9994738f590d319cf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 17 Jun 2021 15:56:39 +0100 Subject: [PATCH 078/184] fix order of operations, set Offset before setting Extent --- src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index b0b52812b9..e3783febdd 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -511,8 +511,8 @@ namespace Avalonia.Controls.Presenters else if (scrollable.IsLogicalScrollEnabled) { Viewport = scrollable.Viewport; - Extent = scrollable.Extent; Offset = scrollable.Offset; + Extent = scrollable.Extent; } } From 955fb1ffdb75e4f77f4e55c1bfdf2d883435c4e9 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 15 Jun 2021 19:01:16 +0200 Subject: [PATCH 079/184] fixes: Warnings CS0169 --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 3 --- src/Avalonia.Controls/Menu.cs | 1 - src/Avalonia.Controls/Repeater/ViewportManager.cs | 1 - src/Avalonia.Controls/Repeater/VirtualizationInfo.cs | 1 - src/Avalonia.X11/TransparencyHelper.cs | 1 - src/Avalonia.X11/X11Window.cs | 1 - src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs | 1 - .../Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs | 1 - src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs | 1 - .../WinRT/Composition/WinUICompositorConnection.cs | 1 - 10 files changed, 12 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 1b4632d368..83f13fe199 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -75,7 +75,6 @@ namespace Avalonia.Controls private const double DATAGRID_defaultMinColumnWidth = 20; private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity; - private List _validationErrors; private List _bindingValidationErrors; private IDisposable _validationSubscription; @@ -102,7 +101,6 @@ namespace Avalonia.Controls private bool _areHandlersSuspended; private bool _autoSizingColumns; private IndexToValueTable _collapsedSlotsTable; - private DataGridCellCoordinates _currentCellCoordinates; private Control _clickedElement; // used to store the current column during a Reset @@ -141,7 +139,6 @@ namespace Avalonia.Controls private DataGridSelectedItemsCollection _selectedItems; private bool _temporarilyResetCurrentCell; private object _uneditedValue; // Represents the original current cell value at the time it enters editing mode. - private ICellEditBinding _currentCellEditBinding; // An approximation of the sum of the heights in pixels of the scrolling rows preceding // the first displayed scrolling row. Since the scrolled off rows are discarded, the grid diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 4da044fec1..706be376a9 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -17,7 +17,6 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); - private LightDismissOverlayLayer? _overlay; /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index 6e24408aa9..da3c2b15e6 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -27,7 +27,6 @@ namespace Avalonia.Controls private IScrollAnchorProvider _scroller; private IControl _makeAnchorElement; private bool _isAnchorOutsideRealizedRange; - private Task _cacheBuildAction; private Rect _visibleWindow; private Rect _layoutExtent; // This is the expected shift by the layout. diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs index f8cfde609e..7e6b24f1b5 100644 --- a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -27,7 +27,6 @@ namespace Avalonia.Controls internal class VirtualizationInfo { private int _pinCounter; - private object _data; public Rect ArrangeBounds { get; set; } public bool AutoRecycleCandidate { get; set; } diff --git a/src/Avalonia.X11/TransparencyHelper.cs b/src/Avalonia.X11/TransparencyHelper.cs index 0578680136..2140b61b6f 100644 --- a/src/Avalonia.X11/TransparencyHelper.cs +++ b/src/Avalonia.X11/TransparencyHelper.cs @@ -10,7 +10,6 @@ namespace Avalonia.X11 private readonly X11Globals _globals; private WindowTransparencyLevel _currentLevel; private WindowTransparencyLevel _requestedLevel; - private bool _isCompositing; private bool _blurAtomsAreSet; public Action TransparencyLevelChanged { get; set; } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 5ac4c4c9d0..37260aa78b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -30,7 +30,6 @@ namespace Avalonia.X11 ITopLevelImplWithTextInputMethod { private readonly AvaloniaX11Platform _platform; - private readonly IWindowImpl _popupParent; private readonly bool _popup; private readonly X11Info _x11; private XConfigureEvent? _configure; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 4bbb58e53e..ac2fd40c54 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -15,7 +15,6 @@ namespace Avalonia.LinuxFramebuffer private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; - private bool _renderQueued; public IInputRoot InputRoot { get; private set; } public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs index b69b151c3b..c35a3d1174 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs @@ -7,7 +7,6 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev internal class EvDevSingleTouchScreen : EvDevDeviceHandler { private readonly IScreenInfoProvider _screenInfo; - private readonly int _width, _height; private readonly Matrix _calibration; private input_absinfo _axisX; private input_absinfo _axisY; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index dc44d2d55f..ee4125101c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -16,7 +16,6 @@ namespace Avalonia.LinuxFramebuffer.Output public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface { private DrmCard _card; - private readonly EglGlPlatformSurface _eglPlatformSurface; public PixelSize PixelSize => _mode.Resolution; public double Scaling { get; set; } public IGlContext PrimaryContext => _deferredContext; diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 2aa82436f6..1c3c959acf 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -17,7 +17,6 @@ namespace Avalonia.Win32.WinRT.Composition class WinUICompositorConnection : IRenderTimer { private readonly EglContext _syncContext; - private IntPtr _queue; private ICompositor _compositor; private ICompositor2 _compositor2; private ICompositor5 _compositor5; From 7d863e877f52323cff846d9c0b835c30d54787f9 Mon Sep 17 00:00:00 2001 From: amwx Date: Fri, 18 Jun 2021 21:22:53 -0500 Subject: [PATCH 080/184] Let Data and Stretch be overridden --- src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index 2eee656dc6..60420f2f44 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -112,9 +112,7 @@ + VerticalAlignment="Center" /> + + From 4523450e0447928a11557618154df550b0c9051a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 12 Jun 2021 15:16:25 -0400 Subject: [PATCH 081/184] Improve context flyout/menu sample pages --- .../Pages/ContextFlyoutPage.axaml | 102 ------------- .../Pages/ContextFlyoutPage.axaml.cs | 45 ------ .../Pages/ContextFlyoutPage.xaml | 143 ++++++++++++++++++ .../Pages/ContextFlyoutPage.xaml.cs | 91 +++++++++++ .../ControlCatalog/Pages/ContextMenuPage.xaml | 137 ++++++++++------- .../Pages/ContextMenuPage.xaml.cs | 42 ++++- .../ViewModels/ContextFlyoutPageViewModel.cs | 78 ---------- ...geViewModel.cs => ContextPageViewModel.cs} | 4 +- 8 files changed, 357 insertions(+), 285 deletions(-) delete mode 100644 samples/ControlCatalog/Pages/ContextFlyoutPage.axaml delete mode 100644 samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs create mode 100644 samples/ControlCatalog/Pages/ContextFlyoutPage.xaml create mode 100644 samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs delete mode 100644 samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs rename samples/ControlCatalog/ViewModels/{ContextMenuPageViewModel.cs => ContextPageViewModel.cs} (96%) diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml deleted file mode 100644 index f0e079ad91..0000000000 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - Context Flyout - A right click Flyout that can be applied to any control. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs deleted file mode 100644 index e64d4a2cdd..0000000000 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using ControlCatalog.ViewModels; -using Avalonia.Interactivity; -namespace ControlCatalog.Pages -{ - public class ContextFlyoutPage : UserControl - { - private TextBox _textBox; - - public ContextFlyoutPage() - { - InitializeComponent(); - - var vm = new ContextFlyoutPageViewModel(); - vm.View = this; - DataContext = vm; - - _textBox = this.FindControl("TextBox"); - - var cutButton = this.FindControl + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs new file mode 100644 index 0000000000..5126ae91bf --- /dev/null +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs @@ -0,0 +1,91 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; +using Avalonia.Interactivity; +using System; +using System.ComponentModel; + +namespace ControlCatalog.Pages +{ + public class ContextFlyoutPage : UserControl + { + private TextBox _textBox; + + public ContextFlyoutPage() + { + InitializeComponent(); + + DataContext = new ContextPageViewModel(); + + _textBox = this.FindControl("TextBox"); + + var cutButton = this.FindControl public event EventHandler? Opened; + internal event EventHandler? Closing; + public IPopupHost? Host => _openState?.PopupHost; public bool WindowManagerAddShadowHint @@ -567,6 +570,13 @@ namespace Avalonia.Controls.Primitives private void CloseCore() { + var closingArgs = new CancelEventArgs(); + Closing?.Invoke(this, closingArgs); + if (closingArgs.Cancel) + { + return; + } + _isOpenRequested = false; if (_openState is null) { From 8ced83c601caf7bb96c9350b0845277208a41b97 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 13 Jun 2021 02:42:46 -0400 Subject: [PATCH 084/184] Update sample pages --- .../Pages/ContextFlyoutPage.xaml | 43 ++++++++++++------- .../Pages/ContextFlyoutPage.xaml.cs | 2 +- .../Pages/ContextMenuPage.xaml.cs | 2 +- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml index cc17d9c747..0d9026bdd5 100644 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml @@ -13,22 +13,22 @@ + + + + Context Flyout A right click Flyout that can be applied to any control. - - - - - + @@ -56,7 +56,7 @@ - + + + + + + + @@ -181,6 +256,9 @@ + + + diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml index 1985074b0f..71b6ea0713 100644 --- a/samples/RenderDemo/Pages/TransitionsPage.xaml +++ b/samples/RenderDemo/Pages/TransitionsPage.xaml @@ -167,13 +167,80 @@ + + + + + + + + + + + + @@ -202,6 +269,15 @@ + + + + + + + + + From 9f7a5de29edfced86e19a72d1fe015e01bc81fb5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 4 Jul 2021 03:51:28 -0400 Subject: [PATCH 115/184] Add RelativePointAnimator --- .../Animators/RelativePointAnimator.cs | 20 +++++++++++++++++++ .../Transitions/RelativePointTransition.cs | 11 ++++++++++ src/Avalonia.Visuals/RelativePoint.cs | 7 +++++++ 3 files changed, 38 insertions(+) create mode 100644 src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs create mode 100644 src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs new file mode 100644 index 0000000000..40fa4503f0 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that handles properties. + /// + public class RelativePointAnimator : Animator + { + private static readonly PointAnimator s_pointAnimator = new PointAnimator(); + + public override RelativePoint Interpolate(double progress, RelativePoint oldValue, RelativePoint newValue) + { + if (oldValue.Unit != newValue.Unit) + { + return progress >= 1 ? newValue : oldValue; + } + + return new RelativePoint(s_pointAnimator.Interpolate(progress, oldValue.Point, newValue.Point), oldValue.Unit); + } + } +} diff --git a/src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs new file mode 100644 index 0000000000..4a7bfa8384 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs @@ -0,0 +1,11 @@ +using Avalonia.Animation.Animators; + +namespace Avalonia.Animation +{ + /// + /// Transition class that handles with type. + /// + public class RelativePointTransition : AnimatorDrivenTransition + { + } +} diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs index 097ea69be4..497820ec65 100644 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ b/src/Avalonia.Visuals/RelativePoint.cs @@ -1,5 +1,7 @@ using System; using System.Globalization; + +using Avalonia.Animation.Animators; using Avalonia.Utilities; namespace Avalonia @@ -45,6 +47,11 @@ namespace Avalonia private readonly RelativeUnit _unit; + static RelativePoint() + { + Animation.Animation.RegisterAnimator(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType)); + } + /// /// Initializes a new instance of the struct. /// From ab071f1ba0f77e020a0f943be897e63fdf8dd8bc Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 4 Jul 2021 03:52:11 -0400 Subject: [PATCH 116/184] Add IGradientBrushAnimator --- .../Animation/Animators/BaseBrushAnimator.cs | 35 ++++++--- .../Animators/GradientBrushAnimator.cs | 74 +++++++++++++++++++ .../Animators/SolidColorBrushAnimator.cs | 4 +- .../Animation/Transitions/BrushTransition.cs | 32 ++++---- src/Avalonia.Visuals/Media/GradientBrush.cs | 3 + 5 files changed, 120 insertions(+), 28 deletions(-) create mode 100644 src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs index 508891fd72..be674269bf 100644 --- a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs @@ -4,6 +4,8 @@ using System.Reactive.Disposables; using Avalonia.Logging; using Avalonia.Media; +#nullable enable + namespace Avalonia.Animation.Animators { /// @@ -12,9 +14,9 @@ namespace Avalonia.Animation.Animators /// redirect them to the properly registered /// animators in this class. /// - public class BaseBrushAnimator : Animator + public class BaseBrushAnimator : Animator { - private IAnimator _targetAnimator; + private IAnimator? _targetAnimator; private static readonly List<(Func Match, Type AnimatorType)> _brushAnimators = new List<(Func Match, Type AnimatorType)>(); @@ -31,7 +33,7 @@ namespace Avalonia.Animation.Animators /// The type of the animator to instantiate. /// public static void RegisterBrushAnimator(Func condition) - where TAnimator : IAnimator + where TAnimator : IAnimator, new() { _brushAnimators.Insert(0, (condition, typeof(TAnimator))); } @@ -40,20 +42,18 @@ namespace Avalonia.Animation.Animators public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete) { - foreach (var valueType in _brushAnimators) - { - if (!valueType.Match(this[0].Value.GetType())) continue; - - _targetAnimator = (IAnimator)Activator.CreateInstance(valueType.AnimatorType); + _targetAnimator = CreateAnimatorFromType(this[0].Value.GetType()); + if (_targetAnimator != null) + { foreach (var keyframe in this) { _targetAnimator.Add(keyframe); } _targetAnimator.Property = this.Property; - - return _targetAnimator.Apply(animation, control, clock, match, onComplete); + + return _targetAnimator.Apply(animation, control, clock, match, onComplete); } Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log( @@ -64,6 +64,19 @@ namespace Avalonia.Animation.Animators } /// - public override IBrush Interpolate(double progress, IBrush oldValue, IBrush newValue) => null; + public override IBrush? Interpolate(double progress, IBrush? oldValue, IBrush? newValue) => null; + + internal static IAnimator? CreateAnimatorFromType(Type type) + { + foreach (var (match, animatorType) in _brushAnimators) + { + if (!match(type)) + continue; + + return (IAnimator)Activator.CreateInstance(animatorType); + } + + return null; + } } } diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs new file mode 100644 index 0000000000..e51103b9b5 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Avalonia.Data; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace Avalonia.Animation.Animators +{ + /// + /// Animator that handles values. + /// + public class IGradientBrushAnimator : Animator + { + private static readonly RelativePointAnimator s_relativePointAnimator = new RelativePointAnimator(); + private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator(); + + public override IGradientBrush Interpolate(double progress, IGradientBrush oldValue, IGradientBrush newValue) + { + if (oldValue is null || newValue is null + || oldValue.GradientStops.Count != oldValue.GradientStops.Count) + { + return progress >= 1 ? newValue : oldValue; + } + + switch (oldValue) + { + case IRadialGradientBrush oldRadial when newValue is IRadialGradientBrush newRadial: + return new ImmutableRadialGradientBrush( + InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), + s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.SpreadMethod, + s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center), + s_relativePointAnimator.Interpolate(progress, oldRadial.GradientOrigin, newRadial.GradientOrigin), + s_doubleAnimator.Interpolate(progress, oldRadial.Radius, newRadial.Radius)); + + case IConicGradientBrush oldConic when newValue is IConicGradientBrush newConic: + return new ImmutableConicGradientBrush( + InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), + s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.SpreadMethod, + s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center), + s_doubleAnimator.Interpolate(progress, oldConic.Angle, newConic.Angle)); + + case ILinearGradientBrush oldLinear when newValue is ILinearGradientBrush newLinear: + return new ImmutableLinearGradientBrush( + InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), + s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.SpreadMethod, + s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint), + s_relativePointAnimator.Interpolate(progress, oldLinear.EndPoint, newLinear.EndPoint)); + + default: + return progress >= 1 ? newValue : oldValue; + } + } + + public override IDisposable BindAnimation(Animatable control, IObservable instance) + { + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); + } + + private IReadOnlyList InterpolateStops(double progress, IReadOnlyList oldValue, IReadOnlyList newValue) + { + // pool + return oldValue + .Zip(newValue, (f, s) => new ImmutableGradientStop( + s_doubleAnimator.Interpolate(progress, f.Offset, s.Offset), + ColorAnimator.InterpolateCore(progress, f.Color, s.Color))) + .ToArray(); + } + } +} diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs index a56cc1de8c..ba2f2ae766 100644 --- a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs @@ -14,7 +14,7 @@ namespace Avalonia.Animation.Animators { if (oldValue is null || newValue is null) { - return oldValue; + return progress >= 1 ? newValue : oldValue; } return new ImmutableSolidColorBrush(ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color)); @@ -26,7 +26,7 @@ namespace Avalonia.Animation.Animators } } - [Obsolete] + [Obsolete("Use ISolidColorBrushAnimator instead")] public class SolidColorBrushAnimator : Animator { public override SolidColorBrush Interpolate(double progress, SolidColorBrush oldValue, SolidColorBrush newValue) diff --git a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs index cc5af1b4b1..7cc3f597b5 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; + using Avalonia.Animation.Animators; using Avalonia.Animation.Easings; using Avalonia.Media; @@ -9,34 +11,34 @@ namespace Avalonia.Animation { /// /// Transition class that handles with type. - /// Only values of will transition correctly at the moment. /// public class BrushTransition : Transition { - private static readonly ISolidColorBrushAnimator s_animator = new ISolidColorBrushAnimator(); - public override IObservable DoTransition(IObservable progress, IBrush? oldValue, IBrush? newValue) { - var oldSolidColorBrush = TryGetSolidColorBrush(oldValue); - var newSolidColorBrush = TryGetSolidColorBrush(newValue); + var type = oldValue?.GetType() ?? newValue?.GetType(); + if (type == null) + { + return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); + } - if (oldSolidColorBrush != null && newSolidColorBrush != null) + var animator = BaseBrushAnimator.CreateAnimatorFromType(type); + if (animator == null) { - return new AnimatorTransitionObservable( - s_animator, progress, Easing, oldSolidColorBrush, newSolidColorBrush); + return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); } - return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); - } + var animatorType = animator.GetType(); + var animatorGenericArgument = animatorType.BaseType.GetGenericArguments().FirstOrDefault() ?? type; - private static ISolidColorBrush? TryGetSolidColorBrush(IBrush? brush) - { - if (brush is null) + var observableType = typeof(AnimatorTransitionObservable<,>).MakeGenericType(animatorGenericArgument, animatorType); + var observable = Activator.CreateInstance(observableType, animator, progress, Easing, oldValue, newValue) as IObservable; + if (observable == null) { - return Brushes.Transparent; + return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); } - return brush as ISolidColorBrush; + return observable; } private class IncompatibleTransitionObservable : TransitionObservableBase diff --git a/src/Avalonia.Visuals/Media/GradientBrush.cs b/src/Avalonia.Visuals/Media/GradientBrush.cs index 99923b8e06..4fb753a9de 100644 --- a/src/Avalonia.Visuals/Media/GradientBrush.cs +++ b/src/Avalonia.Visuals/Media/GradientBrush.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; + +using Avalonia.Animation.Animators; using Avalonia.Collections; using Avalonia.Metadata; @@ -28,6 +30,7 @@ namespace Avalonia.Media static GradientBrush() { + BaseBrushAnimator.RegisterBrushAnimator(match => typeof(IGradientBrush).IsAssignableFrom(match)); GradientStopsProperty.Changed.Subscribe(GradientStopsChanged); AffectsRender(SpreadMethodProperty); } From a885e673c831fe660323b1ff6e9e2ddce34e6d12 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 4 Jul 2021 20:37:48 -0400 Subject: [PATCH 117/184] Reflection free implementation with automatic convertion from solid color brush to gradient --- samples/RenderDemo/Pages/AnimationsPage.xaml | 34 ++++- .../Animation/Animators/BaseBrushAnimator.cs | 129 +++++++++++++++--- .../Animators/GradientBrushAnimator.cs | 65 +++++++-- .../Animators/SolidColorBrushAnimator.cs | 18 +-- .../Animation/Transitions/BrushTransition.cs | 40 +++--- src/Avalonia.Visuals/Media/GradientBrush.cs | 1 - src/Avalonia.Visuals/Media/SolidColorBrush.cs | 1 - 7 files changed, 225 insertions(+), 63 deletions(-) diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index 48fca61d09..3981f7b51b 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -168,6 +168,9 @@ IterationCount="Infinite" PlaybackDirection="Alternate"> + + + @@ -175,6 +178,9 @@ + + + @@ -188,6 +194,31 @@ + + - @@ -226,16 +237,20 @@ - + + - + - + + + + @@ -247,10 +262,34 @@ + PlaybackDirection="Normal"> - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -258,9 +297,9 @@ - - - + + + diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs index 25b4a2826c..6481f815de 100644 --- a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs @@ -19,8 +19,7 @@ namespace Avalonia.Animation.Animators public override IGradientBrush? Interpolate(double progress, IGradientBrush? oldValue, IGradientBrush? newValue) { - if (oldValue is null || newValue is null - || oldValue.GradientStops.Count != newValue.GradientStops.Count) + if (oldValue is null || newValue is null) { return progress >= 0.5 ? newValue : oldValue; } @@ -64,13 +63,26 @@ namespace Avalonia.Animation.Animators private IReadOnlyList InterpolateStops(double progress, IReadOnlyList oldValue, IReadOnlyList newValue) { - var stops = new ImmutableGradientStop[oldValue.Count]; - for (int index = 0; index < oldValue.Count; index++) + var resultCount = Math.Max(oldValue.Count, newValue.Count); + var stops = new ImmutableGradientStop[resultCount]; + + for (int index = 0, oldIndex = 0, newIndex = 0; index < resultCount; index++) { stops[index] = new ImmutableGradientStop( - s_doubleAnimator.Interpolate(progress, oldValue[index].Offset, newValue[index].Offset), - ColorAnimator.InterpolateCore(progress, oldValue[index].Color, newValue[index].Color)); + s_doubleAnimator.Interpolate(progress, oldValue[oldIndex].Offset, newValue[newIndex].Offset), + ColorAnimator.InterpolateCore(progress, oldValue[oldIndex].Color, newValue[newIndex].Color)); + + if (oldIndex < oldValue.Count - 1) + { + oldIndex++; + } + + if (newIndex < newValue.Count - 1) + { + newIndex++; + } } + return stops; } @@ -80,29 +92,29 @@ namespace Avalonia.Animation.Animators { case IRadialGradientBrush oldRadial: return new ImmutableRadialGradientBrush( - CreateStopsFromSolidColorBrush(solidColorBrush, oldRadial), solidColorBrush.Opacity, + CreateStopsFromSolidColorBrush(solidColorBrush, oldRadial.GradientStops), solidColorBrush.Opacity, oldRadial.SpreadMethod, oldRadial.Center, oldRadial.GradientOrigin, oldRadial.Radius); case IConicGradientBrush oldConic: return new ImmutableConicGradientBrush( - CreateStopsFromSolidColorBrush(solidColorBrush, oldConic), solidColorBrush.Opacity, + CreateStopsFromSolidColorBrush(solidColorBrush, oldConic.GradientStops), solidColorBrush.Opacity, oldConic.SpreadMethod, oldConic.Center, oldConic.Angle); case ILinearGradientBrush oldLinear: return new ImmutableLinearGradientBrush( - CreateStopsFromSolidColorBrush(solidColorBrush, oldLinear), solidColorBrush.Opacity, + CreateStopsFromSolidColorBrush(solidColorBrush, oldLinear.GradientStops), solidColorBrush.Opacity, oldLinear.SpreadMethod, oldLinear.StartPoint, oldLinear.EndPoint); default: throw new NotSupportedException($"Gradient of type {gradientBrush?.GetType()} is not supported"); } - static IReadOnlyList CreateStopsFromSolidColorBrush(ISolidColorBrush solidColorBrush, IGradientBrush baseGradient) + static IReadOnlyList CreateStopsFromSolidColorBrush(ISolidColorBrush solidColorBrush, IReadOnlyList baseStops) { - var stops = new ImmutableGradientStop[baseGradient.GradientStops.Count]; - for (int index = 0; index < baseGradient.GradientStops.Count; index++) + var stops = new ImmutableGradientStop[baseStops.Count]; + for (int index = 0; index < baseStops.Count; index++) { - stops[index] = new ImmutableGradientStop(baseGradient.GradientStops[index].Offset, solidColorBrush.Color); + stops[index] = new ImmutableGradientStop(baseStops[index].Offset, solidColorBrush.Color); } return stops; } From 17c94d3cf3b43d71aea697f9a17c264ac80da87b Mon Sep 17 00:00:00 2001 From: amwx Date: Sun, 4 Jul 2021 21:33:52 -0500 Subject: [PATCH 119/184] Add impl from upstream --- src/Avalonia.Layout/ElementManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs index cb13deb15f..3f106708e6 100644 --- a/src/Avalonia.Layout/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -325,7 +325,10 @@ namespace Avalonia.Layout break; case NotifyCollectionChangedAction.Move: - throw new NotImplementedException(); + int size = args.OldItems != null ? args.OldItems.Count : 1; + OnItemsRemoved(args.OldStartingIndex, size); + OnItemsAdded(args.NewStartingIndex, size); + break; } } } From e8da78cd41fffb23453567dc3c391f78101a095e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 5 Jul 2021 14:18:12 -0400 Subject: [PATCH 120/184] Update relative point animation --- .../Animation/Animators/RelativePointAnimator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs index 40fa4503f0..348a2e4a35 100644 --- a/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs @@ -11,7 +11,7 @@ { if (oldValue.Unit != newValue.Unit) { - return progress >= 1 ? newValue : oldValue; + return progress >= 0.5 ? newValue : oldValue; } return new RelativePoint(s_pointAnimator.Interpolate(progress, oldValue.Point, newValue.Point), oldValue.Unit); From ea3f85e1263cfb75ce2452e205477080d1c9b531 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Jul 2021 09:34:18 +0200 Subject: [PATCH 121/184] Don't allow using a closed window as a parent/owner. --- src/Avalonia.Controls/Window.cs | 10 +++++++ .../WindowTests.cs | 30 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 700c3d9bad..2d369dae8c 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -635,6 +635,11 @@ namespace Avalonia.Controls throw new InvalidOperationException("Cannot re-show a closed window."); } + if (parent != null && parent.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot Show a Window with a closed parent."); + } + if (IsVisible) { return; @@ -709,6 +714,11 @@ namespace Avalonia.Controls throw new ArgumentNullException(nameof(owner)); } + if (owner.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot Show a Window with a closed owner."); + } + if (IsVisible) { throw new InvalidOperationException("The window is already being shown."); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index e8311b79ac..2d4559ddba 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -387,6 +387,36 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Calling_Show_With_Closed_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + parent.Close(); + + var ex = Assert.Throws(() => target.Show(parent)); + Assert.Equal("Cannot Show a Window with a closed parent.", ex.Message); + } + } + + [Fact] + public async Task Calling_ShowDialog_With_Closed_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + parent.Close(); + + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); + Assert.Equal("Cannot Show a Window with a closed owner.", ex.Message); + } + } + [Fact] public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen() { From 69852a56f588fe0e744aa11e7577eeb351f29670 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Jul 2021 12:16:44 +0200 Subject: [PATCH 122/184] Don't allow self as parent/owner window. --- src/Avalonia.Controls/Window.cs | 17 +++++++++++-- .../WindowTests.cs | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 2d369dae8c..9f6c605d46 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -635,9 +635,17 @@ namespace Avalonia.Controls throw new InvalidOperationException("Cannot re-show a closed window."); } - if (parent != null && parent.PlatformImpl == null) + if (parent != null) { - throw new InvalidOperationException("Cannot Show a Window with a closed parent."); + if (parent.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot Show a Window with a closed parent."); + } + + if (parent == this) + { + throw new InvalidOperationException("A Window cannot be its own parent."); + } } if (IsVisible) @@ -719,6 +727,11 @@ namespace Avalonia.Controls throw new InvalidOperationException("Cannot Show a Window with a closed owner."); } + if (owner == this) + { + throw new InvalidOperationException("A Window cannot be its own owner."); + } + if (IsVisible) { throw new InvalidOperationException("The window is already being shown."); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 2d4559ddba..5ba529292f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -417,6 +417,30 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Calling_Show_With_Self_As_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window(); + + var ex = Assert.Throws(() => target.Show(target)); + Assert.Equal("A Window cannot be its own parent.", ex.Message); + } + } + + [Fact] + public async Task Calling_ShowDialog_With_Self_As_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window(); + + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(target)); + Assert.Equal("A Window cannot be its own owner.", ex.Message); + } + } + [Fact] public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen() { From 15cdee1bee18cc7466f68951f7bfa863fee748be Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Jul 2021 13:27:01 +0200 Subject: [PATCH 123/184] Ensure parent/owner windows are visible when showing child. --- src/Avalonia.Controls/Window.cs | 24 +++++---- .../WindowTests.cs | 50 +++++++++++++++---- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 9f6c605d46..ddda083aa8 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -639,13 +639,16 @@ namespace Avalonia.Controls { if (parent.PlatformImpl == null) { - throw new InvalidOperationException("Cannot Show a Window with a closed parent."); + throw new InvalidOperationException("Cannot show a window with a closed parent."); } - - if (parent == this) + else if (parent == this) { throw new InvalidOperationException("A Window cannot be its own parent."); } + else if (!parent.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } } if (IsVisible) @@ -721,21 +724,22 @@ namespace Avalonia.Controls { throw new ArgumentNullException(nameof(owner)); } - - if (owner.PlatformImpl == null) + else if (owner.PlatformImpl == null) { - throw new InvalidOperationException("Cannot Show a Window with a closed owner."); + throw new InvalidOperationException("Cannot show a window with a closed owner."); } - - if (owner == this) + else if (owner == this) { throw new InvalidOperationException("A Window cannot be its own owner."); } - - if (IsVisible) + else if (IsVisible) { throw new InvalidOperationException("The window is already being shown."); } + else if (!owner.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 5ba529292f..88c6c86c46 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -279,10 +279,11 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = Mock.Of(); + var parent = new Window(); var renderer = new Mock(); var target = new Window(CreateImpl(renderer)); + parent.Show(); target.ShowDialog(parent); renderer.Verify(x => x.Start(), Times.Once); @@ -294,10 +295,11 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = Mock.Of(); + var parent = new Window(); var target = new Window(); var raised = false; + parent.Show(); target.Opened += (s, e) => raised = true; target.ShowDialog(parent); @@ -326,14 +328,15 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = new Mock(); + var parent = new Window(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); + parent.Show(); var target = new Window(windowImpl.Object); - var task = target.ShowDialog(parent.Object); + var task = target.ShowDialog(parent); windowImpl.Object.Closed(); @@ -366,14 +369,16 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = new Mock(); + var parent = new Window(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); + parent.Show(); + var target = new Window(windowImpl.Object); - var task = target.ShowDialog(parent.Object); + var task = target.ShowDialog(parent); windowImpl.Object.Closed(); await task; @@ -381,7 +386,7 @@ namespace Avalonia.Controls.UnitTests var openedRaised = false; target.Opened += (s, e) => openedRaised = true; - var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent.Object)); + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); Assert.Equal("Cannot re-show a closed window.", ex.Message); Assert.False(openedRaised); } @@ -398,7 +403,7 @@ namespace Avalonia.Controls.UnitTests parent.Close(); var ex = Assert.Throws(() => target.Show(parent)); - Assert.Equal("Cannot Show a Window with a closed parent.", ex.Message); + Assert.Equal("Cannot show a window with a closed parent.", ex.Message); } } @@ -413,7 +418,33 @@ namespace Avalonia.Controls.UnitTests parent.Close(); var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); - Assert.Equal("Cannot Show a Window with a closed owner.", ex.Message); + Assert.Equal("Cannot show a window with a closed owner.", ex.Message); + } + } + + [Fact] + public void Calling_Show_With_Invisible_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + var ex = Assert.Throws(() => target.Show(parent)); + Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + } + } + + [Fact] + public async Task Calling_ShowDialog_With_Invisible_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); + Assert.Equal("Cannot show window with non-visible parent.", ex.Message); } } @@ -740,6 +771,7 @@ namespace Avalonia.Controls.UnitTests protected override void Show(Window window) { var owner = new Window(); + owner.Show(); window.ShowDialog(owner); } } From a42334d128eb919c6f29de6c7d51cd333b4221a0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Jul 2021 14:00:09 +0200 Subject: [PATCH 124/184] Hide child windows when hiding parent/owner. --- src/Avalonia.Controls/Window.cs | 8 +++++ .../WindowTests.cs | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ddda083aa8..ae314a33ce 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -592,6 +592,14 @@ namespace Avalonia.Controls owner.RemoveChild(this); } + if (_children.Count > 0) + { + foreach (var child in _children.ToArray()) + { + child.child.Hide(); + } + } + Owner = null; PlatformImpl?.Hide(); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 88c6c86c46..6b9921d83d 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -472,6 +472,42 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Hiding_Parent_Window_Should_Close_Children() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var parent = new Window(); + var child = new Window(); + + parent.Show(); + child.Show(parent); + + parent.Hide(); + + Assert.False(parent.IsVisible); + Assert.False(child.IsVisible); + } + } + + [Fact] + public void Hiding_Parent_Window_Should_Close_Dialog_Children() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var parent = new Window(); + var child = new Window(); + + parent.Show(); + child.ShowDialog(parent); + + parent.Hide(); + + Assert.False(parent.IsVisible); + Assert.False(child.IsVisible); + } + } + [Fact] public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen() { From f9824007e1a3f47a2583bd86f819bd6cccc9f3b7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 6 Jul 2021 20:26:23 +0100 Subject: [PATCH 125/184] add a method to retain com object during com calls. --- native/Avalonia.Native/inc/comimpl.h | 184 ++++++++++++++------------- 1 file changed, 95 insertions(+), 89 deletions(-) diff --git a/native/Avalonia.Native/inc/comimpl.h b/native/Avalonia.Native/inc/comimpl.h index 0ff64b7215..c9f70cfa3b 100644 --- a/native/Avalonia.Native/inc/comimpl.h +++ b/native/Avalonia.Native/inc/comimpl.h @@ -10,6 +10,95 @@ __IID_DEF(IUnknown, 0, 0, 0, C0, 00, 00, 00, 00, 00, 00, 46); +template +class ComPtr +{ +private: + TInterface* _obj; +public: + ComPtr() + { + _obj = 0; + } + + ComPtr(TInterface* pObj) + { + _obj = 0; + + if (pObj) + { + _obj = pObj; + _obj->AddRef(); + } + } + + ComPtr(const ComPtr& ptr) + { + _obj = 0; + + if (ptr._obj) + { + _obj = ptr._obj; + _obj->AddRef(); + } + + } + + ComPtr& operator=(ComPtr other) + { + if(_obj != NULL) + _obj->Release(); + _obj = other._obj; + if(_obj != NULL) + _obj->AddRef(); + return *this; + } + + ~ComPtr() + { + if (_obj) + { + _obj->Release(); + _obj = 0; + } + } + + TInterface* getRaw() + { + return _obj; + } + + TInterface* getRetainedReference() + { + if(_obj == NULL) + return NULL; + _obj->AddRef(); + return _obj; + } + + TInterface** getPPV() + { + return &_obj; + } + + operator TInterface*() const + { + return _obj; + } + TInterface& operator*() const + { + return *_obj; + } + TInterface** operator&() + { + return &_obj; + } + TInterface* operator->() const + { + return _obj; + } +}; + class ComObject : public virtual IUnknown { private: @@ -58,6 +147,12 @@ public: _refCount++; return S_OK; } + +protected: + ComPtr UnknownSelf() + { + return this; + } }; @@ -104,94 +199,5 @@ public: virtual ~ComSingleObject(){} }; -template -class ComPtr -{ -private: - TInterface* _obj; -public: - ComPtr() - { - _obj = 0; - } - - ComPtr(TInterface* pObj) - { - _obj = 0; - - if (pObj) - { - _obj = pObj; - _obj->AddRef(); - } - } - - ComPtr(const ComPtr& ptr) - { - _obj = 0; - - if (ptr._obj) - { - _obj = ptr._obj; - _obj->AddRef(); - } - - } - - ComPtr& operator=(ComPtr other) - { - if(_obj != NULL) - _obj->Release(); - _obj = other._obj; - if(_obj != NULL) - _obj->AddRef(); - return *this; - } - - ~ComPtr() - { - if (_obj) - { - _obj->Release(); - _obj = 0; - } - } - - TInterface* getRaw() - { - return _obj; - } - - TInterface* getRetainedReference() - { - if(_obj == NULL) - return NULL; - _obj->AddRef(); - return _obj; - } - - TInterface** getPPV() - { - return &_obj; - } - - operator TInterface*() const - { - return _obj; - } - TInterface& operator*() const - { - return *_obj; - } - TInterface** operator&() - { - return &_obj; - } - TInterface* operator->() const - { - return _obj; - } -}; - #endif // COMIMPL_H_INCLUDED #pragma clang diagnostic pop From 8058a46d260b1c6d173eb5cc775506f2c0f83565 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 6 Jul 2021 20:27:04 +0100 Subject: [PATCH 126/184] prevent osx crash when closing inside activate. --- native/Avalonia.Native/src/OSX/window.mm | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index c0936356d2..7bbff96270 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -113,9 +113,11 @@ public: UpdateStyle(); [Window setContentView: StandardContainer]; + [Window setTitle:_lastTitle]; if(ShouldTakeFocusOnShow() && activate) { + [Window orderFront: Window]; [Window makeKeyAndOrderFront:Window]; [NSApp activateIgnoringOtherApps:YES]; } @@ -123,7 +125,6 @@ public: { [Window orderFront: Window]; } - [Window setTitle:_lastTitle]; _shown = true; @@ -180,7 +181,10 @@ public: { if (Window != nullptr) { - [Window close]; + auto window = Window; + Window = nullptr; + + [window close]; } return S_OK; @@ -549,6 +553,11 @@ private: void HideOrShowTrafficLights () { + if (Window == nil) + { + return; + } + for (id subview in Window.contentView.superview.subviews) { if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { NSView *titlebarView = [subview subviews][0]; @@ -963,6 +972,11 @@ private: virtual HRESULT SetWindowState (AvnWindowState state) override { + if(Window == nullptr) + { + return S_OK; + } + @autoreleasepool { if(_actualWindowState == state) From 57440e9d36c9bbb2825cc56d688b02ecb21abc91 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 6 Jul 2021 20:27:18 +0100 Subject: [PATCH 127/184] retain self (this) --- native/Avalonia.Native/src/OSX/window.mm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 7bbff96270..439f6710b9 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -585,7 +585,9 @@ private: virtual HRESULT Show (bool activate) override { @autoreleasepool - { + { + auto r = this->UnknownSelf(); + WindowBaseImpl::Show(activate); HideOrShowTrafficLights(); @@ -1925,7 +1927,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if(![self windowShouldClose:self]) return; } - + [self close]; } From cee1a24e0330d4daedea663e858ee33d761d2227 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 7 Jul 2021 17:06:59 +0200 Subject: [PATCH 128/184] Only try to close non-owned windows on shutdown. Owned windows will be closed by their owners. --- .../ClassicDesktopStyleApplicationLifetime.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 2c43f13c82..2256f4cb54 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -132,8 +132,12 @@ namespace Avalonia.Controls.ApplicationLifetimes private void ShutdownRequested(object sender, CancelEventArgs e) { + // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel + // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their + // owners. foreach (var w in Windows) - w.Close(); + if (w.Owner is null) + w.Close(); if (Windows.Count > 0) e.Cancel = true; } From f2907c4f49ff1f8b0d77e828154d49342d36178f Mon Sep 17 00:00:00 2001 From: Royce551 Date: Wed, 7 Jul 2021 22:54:10 -0500 Subject: [PATCH 129/184] Implement ItemsControl.IsTextSearchEnabled --- src/Avalonia.Controls/ItemsControl.cs | 69 +++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 20d032f597..c544f753e5 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; @@ -12,6 +13,7 @@ using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -52,6 +54,11 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); + public static readonly StyledProperty IsTextSearchEnabledProperty = + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); + + private string _textSearchTerm = string.Empty; + private DispatcherTimer _textSearchTimer; private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; @@ -135,6 +142,12 @@ namespace Avalonia.Controls set { SetValue(ItemTemplateProperty, value); } } + public bool IsTextSearchEnabled + { + get { return GetValue(IsTextSearchEnabledProperty); } + set { SetValue(IsTextSearchEnabledProperty, value); } + } + /// /// Gets the items presenter control. /// @@ -323,6 +336,36 @@ namespace Avalonia.Controls base.OnKeyDown(e); } + protected override void OnTextInput(TextInputEventArgs e) + { + if (!e.Handled && this is SelectingItemsControl selectingItemsControl) + { + if (!IsTextSearchEnabled) + return; + + StopTextSearchTimer(); + + _textSearchTerm += e.Text; + + bool match(ItemContainerInfo info) => + info.ContainerControl is IContentControl control && + control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + + var info = ItemContainerGenerator.Containers.FirstOrDefault(match); + + if (info != null) + { + selectingItemsControl.SelectedIndex = info.Index; + } + + StartTextSearchTimer(); + + e.Handled = true; + } + + base.OnTextInput(e); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -456,6 +499,32 @@ namespace Avalonia.Controls } } + private void StartTextSearchTimer() + { + _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _textSearchTimer.Tick += TextSearchTimer_Tick; + _textSearchTimer.Start(); + } + + private void StopTextSearchTimer() + { + if (_textSearchTimer == null) + { + return; + } + + _textSearchTimer.Stop(); + _textSearchTimer.Tick -= TextSearchTimer_Tick; + + _textSearchTimer = null; + } + + private void TextSearchTimer_Tick(object sender, EventArgs e) + { + _textSearchTerm = string.Empty; + StopTextSearchTimer(); + } + private void UpdateItemCount() { if (Items == null) From 875655e3f3bd051fbb1840d6c8f3bab03891d255 Mon Sep 17 00:00:00 2001 From: Royce551 Date: Wed, 7 Jul 2021 23:04:43 -0500 Subject: [PATCH 130/184] Remove IsTextSearchEnabled implemenetation from ComboBox --- src/Avalonia.Controls/ComboBox.cs | 69 ------------------------------- 1 file changed, 69 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index dd2109e6cd..89cfb5fa8f 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -77,14 +77,6 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly StyledProperty IsTextSearchEnabledProperty = - AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); - - private string _textSearchTerm = string.Empty; - private DispatcherTimer _textSearchTimer; private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; @@ -173,15 +165,6 @@ namespace Avalonia.Controls set { SetValue(VerticalContentAlignmentProperty, value); } } - /// - /// Gets or sets a value that specifies whether a user can jump to a value by typing. - /// - public bool IsTextSearchEnabled - { - get { return GetValue(IsTextSearchEnabledProperty); } - set { SetValue(IsTextSearchEnabledProperty, value); } - } - /// protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -247,32 +230,6 @@ namespace Avalonia.Controls } } - /// - protected override void OnTextInput(TextInputEventArgs e) - { - if (!IsTextSearchEnabled || e.Handled) - return; - - StopTextSearchTimer(); - - _textSearchTerm += e.Text; - - bool match(ItemContainerInfo info) => - info.ContainerControl is IContentControl control && - control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; - - var info = ItemContainerGenerator.Containers.FirstOrDefault(match); - - if (info != null) - { - SelectedIndex = info.Index; - } - - StartTextSearchTimer(); - - e.Handled = true; - } - /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { @@ -470,31 +427,5 @@ namespace Avalonia.Controls SelectedIndex = prev; } - - private void StartTextSearchTimer() - { - _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; - _textSearchTimer.Tick += TextSearchTimer_Tick; - _textSearchTimer.Start(); - } - - private void StopTextSearchTimer() - { - if (_textSearchTimer == null) - { - return; - } - - _textSearchTimer.Stop(); - _textSearchTimer.Tick -= TextSearchTimer_Tick; - - _textSearchTimer = null; - } - - private void TextSearchTimer_Tick(object sender, EventArgs e) - { - _textSearchTerm = string.Empty; - StopTextSearchTimer(); - } } } From 4653c8ff607b7d82c8535472f0d0fecfdc67f302 Mon Sep 17 00:00:00 2001 From: Royce551 Date: Wed, 7 Jul 2021 23:14:13 -0500 Subject: [PATCH 131/184] Readd xmldocs --- src/Avalonia.Controls/ItemsControl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index c544f753e5..c86f2e591b 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -54,6 +54,9 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); + /// + /// Defines the property. + /// public static readonly StyledProperty IsTextSearchEnabledProperty = AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); @@ -142,6 +145,9 @@ namespace Avalonia.Controls set { SetValue(ItemTemplateProperty, value); } } + /// + /// Gets or sets a value that specifies whether a user can jump to a value by typing. + /// public bool IsTextSearchEnabled { get { return GetValue(IsTextSearchEnabledProperty); } From f5ded2632133803632116efad9e5465f4811fee8 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Thu, 8 Jul 2021 13:07:59 +0800 Subject: [PATCH 132/184] Update Arc.cs --- src/Avalonia.Controls/Shapes/Arc.cs | 47 +++++++++++++++-------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Arc.cs b/src/Avalonia.Controls/Shapes/Arc.cs index 4ce72319aa..dad47875c3 100644 --- a/src/Avalonia.Controls/Shapes/Arc.cs +++ b/src/Avalonia.Controls/Shapes/Arc.cs @@ -28,33 +28,34 @@ namespace Avalonia.Controls.Shapes /// public double StartAngle { - get { return GetValue(StartAngleProperty); } - set { SetValue(StartAngleProperty, value); } + get => GetValue(StartAngleProperty); + set => SetValue(StartAngleProperty, value); } /// - /// Gets or sets the angle, in degrees, added to the defining where the arc ends. A positive value is clockwise, negative is counter-clockwise. + /// Gets or sets the angle, in degrees, added to the defining where the arc ends. + /// A positive value is clockwise, negative is counter-clockwise. /// public double SweepAngle { - get { return GetValue(SweepAngleProperty); } - set { SetValue(SweepAngleProperty, value); } + get => GetValue(SweepAngleProperty); + set => SetValue(SweepAngleProperty, value); } protected override Geometry CreateDefiningGeometry() { - double angle1 = DegreesToRad(StartAngle); - double angle2 = angle1 + DegreesToRad(SweepAngle); + var angle1 = DegreesToRad(StartAngle); + var angle2 = angle1 + DegreesToRad(SweepAngle); - double startAngle = Math.Min(angle1, angle2); - double sweepAngle = Math.Max(angle1, angle2); + var startAngle = Math.Min(angle1, angle2); + var sweepAngle = Math.Max(angle1, angle2); - double normStart = RadToNormRad(startAngle); - double normEnd = RadToNormRad(sweepAngle); + var normStart = RadToNormRad(startAngle); + var normEnd = RadToNormRad(sweepAngle); var rect = new Rect(Bounds.Size); - if ((normStart == normEnd) && (startAngle != sweepAngle)) // complete ring + if ((normStart == normEnd) && (startAngle != sweepAngle)) // Complete ring. { return new EllipseGeometry(rect.Deflate(StrokeThickness / 2)); } @@ -62,28 +63,30 @@ namespace Avalonia.Controls.Shapes { return new StreamGeometry(); } - else // partial arc + else // Partial arc. { var deflatedRect = rect.Deflate(StrokeThickness / 2); - double centerX = rect.Center.X; - double centerY = rect.Center.Y; + var centerX = rect.Center.X; + var centerY = rect.Center.Y; - double radiusX = deflatedRect.Width / 2; - double radiusY = deflatedRect.Height / 2; + var radiusX = deflatedRect.Width / 2; + var radiusY = deflatedRect.Height / 2; - double angleGap = RadToNormRad(sweepAngle - startAngle); + var angleGap = RadToNormRad(sweepAngle - startAngle); - Point startPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, startAngle); - Point endPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, sweepAngle); + var startPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, startAngle); + var endPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, sweepAngle); - StreamGeometry arcGeometry = new StreamGeometry(); + var arcGeometry = new StreamGeometry(); + using (var ctx = arcGeometry.Open()) { ctx.BeginFigure(startPoint, false); ctx.ArcTo(endPoint, new Size(radiusX, radiusY), angleGap, angleGap >= Math.PI, SweepDirection.Clockwise); ctx.EndFigure(false); } + return arcGeometry; } } @@ -91,7 +94,7 @@ namespace Avalonia.Controls.Shapes static double DegreesToRad(double inAngle) => inAngle * Math.PI / 180; - static double RadToNormRad(double inAngle) => (0 + (inAngle % (Math.PI * 2)) + (Math.PI * 2)) % (Math.PI * 2); + static double RadToNormRad(double inAngle) => (inAngle % (Math.PI * 2)) + (Math.PI * 2)) % (Math.PI * 2); static Point GetRingPoint(double radiusX, double radiusY, double centerX, double centerY, double angle) => new Point((radiusX * Math.Cos(angle)) + centerX, (radiusY * Math.Sin(angle)) + centerY); From 8d0a4ff5ea5ca84304891a528f481f0c410bc2a1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 8 Jul 2021 02:31:39 -0400 Subject: [PATCH 133/184] Rename GradientBrushAnimator --- .../Animation/Animators/BaseBrushAnimator.cs | 8 ++++---- .../Animation/Animators/GradientBrushAnimator.cs | 2 +- .../Animation/Transitions/BrushTransition.cs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs index 38da394088..ba7a3868c7 100644 --- a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs @@ -78,21 +78,21 @@ namespace Avalonia.Animation.Animators return false; } - var gradientAnimator = new IGradientBrushAnimator(); + var gradientAnimator = new GradientBrushAnimator(); gradientAnimator.Property = Property; foreach (var keyframe in this) { if (keyframe.Value is ISolidColorBrush solidColorBrush) { - gradientAnimator.Add(new AnimatorKeyFrame(typeof(IGradientBrushAnimator), keyframe.Cue, keyframe.KeySpline) + gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline) { - Value = IGradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush) + Value = GradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush) }); } else if (keyframe.Value is IGradientBrush) { - gradientAnimator.Add(new AnimatorKeyFrame(typeof(IGradientBrushAnimator), keyframe.Cue, keyframe.KeySpline) + gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline) { Value = keyframe.Value }); diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs index 6481f815de..864e12413f 100644 --- a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs @@ -12,7 +12,7 @@ namespace Avalonia.Animation.Animators /// /// Animator that handles values. /// - public class IGradientBrushAnimator : Animator + public class GradientBrushAnimator : Animator { private static readonly RelativePointAnimator s_relativePointAnimator = new RelativePointAnimator(); private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator(); diff --git a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs index 2229d6edc6..4d9c8af4d5 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs @@ -13,7 +13,7 @@ namespace Avalonia.Animation /// public class BrushTransition : Transition { - private static readonly IGradientBrushAnimator s_gradientAnimator = new IGradientBrushAnimator(); + private static readonly GradientBrushAnimator s_gradientAnimator = new GradientBrushAnimator(); private static readonly ISolidColorBrushAnimator s_solidColorBrushAnimator = new ISolidColorBrushAnimator(); public override IObservable DoTransition(IObservable progress, IBrush? oldValue, IBrush? newValue) @@ -27,18 +27,18 @@ namespace Avalonia.Animation { if (newValue is IGradientBrush newGradient) { - return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, oldGradient, newGradient); + return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, oldGradient, newGradient); } else if (newValue is ISolidColorBrush newSolidColorBrushToConvert) { - var convertedSolidColorBrush = IGradientBrushAnimator.ConvertSolidColorBrushToGradient(oldGradient, newSolidColorBrushToConvert); - return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, oldGradient, convertedSolidColorBrush); + var convertedSolidColorBrush = GradientBrushAnimator.ConvertSolidColorBrushToGradient(oldGradient, newSolidColorBrushToConvert); + return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, oldGradient, convertedSolidColorBrush); } } else if (newValue is IGradientBrush newGradient && oldValue is ISolidColorBrush oldSolidColorBrushToConvert) { - var convertedSolidColorBrush = IGradientBrushAnimator.ConvertSolidColorBrushToGradient(newGradient, oldSolidColorBrushToConvert); - return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, convertedSolidColorBrush, newGradient); + var convertedSolidColorBrush = GradientBrushAnimator.ConvertSolidColorBrushToGradient(newGradient, oldSolidColorBrushToConvert); + return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, convertedSolidColorBrush, newGradient); } if (oldValue is ISolidColorBrush oldSolidColorBrush && newValue is ISolidColorBrush newSolidColorBrush) From 3740c9d9733693e118454aca541c671bce110a85 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Thu, 8 Jul 2021 14:33:18 +0800 Subject: [PATCH 134/184] Update Arc.cs --- src/Avalonia.Controls/Shapes/Arc.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Arc.cs b/src/Avalonia.Controls/Shapes/Arc.cs index dad47875c3..5ebb321f9b 100644 --- a/src/Avalonia.Controls/Shapes/Arc.cs +++ b/src/Avalonia.Controls/Shapes/Arc.cs @@ -41,12 +41,12 @@ namespace Avalonia.Controls.Shapes get => GetValue(SweepAngleProperty); set => SetValue(SweepAngleProperty, value); } - + protected override Geometry CreateDefiningGeometry() { var angle1 = DegreesToRad(StartAngle); var angle2 = angle1 + DegreesToRad(SweepAngle); - + var startAngle = Math.Min(angle1, angle2); var sweepAngle = Math.Max(angle1, angle2); @@ -72,29 +72,30 @@ namespace Avalonia.Controls.Shapes var radiusX = deflatedRect.Width / 2; var radiusY = deflatedRect.Height / 2; - + var angleGap = RadToNormRad(sweepAngle - startAngle); var startPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, startAngle); var endPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, sweepAngle); var arcGeometry = new StreamGeometry(); - + using (var ctx = arcGeometry.Open()) { ctx.BeginFigure(startPoint, false); - ctx.ArcTo(endPoint, new Size(radiusX, radiusY), angleGap, angleGap >= Math.PI, SweepDirection.Clockwise); + ctx.ArcTo(endPoint, new Size(radiusX, radiusY), angleGap, angleGap >= Math.PI, + SweepDirection.Clockwise); ctx.EndFigure(false); } - + return arcGeometry; } } static double DegreesToRad(double inAngle) => inAngle * Math.PI / 180; - - static double RadToNormRad(double inAngle) => (inAngle % (Math.PI * 2)) + (Math.PI * 2)) % (Math.PI * 2); + + static double RadToNormRad(double inAngle) => ((inAngle % (Math.PI * 2)) + (Math.PI * 2)) % (Math.PI * 2); static Point GetRingPoint(double radiusX, double radiusY, double centerX, double centerY, double angle) => new Point((radiusX * Math.Cos(angle)) + centerX, (radiusY * Math.Sin(angle)) + centerY); From ec85bf14e602c3e0eaed87a7761b1c09fd43ea17 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 8 Jul 2021 11:41:19 +0200 Subject: [PATCH 135/184] Call makeFirstResponder when showing window. This is needed in order for the view to receive keyboard events immediately after showing the window. Fixes #6202 --- native/Avalonia.Native/src/OSX/window.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index e40ec12461..17d9a150a8 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -117,6 +117,7 @@ public: if(ShouldTakeFocusOnShow() && activate) { [Window makeKeyAndOrderFront:Window]; + [Window makeFirstResponder:View]; [NSApp activateIgnoringOtherApps:YES]; } else From ac455785755a54811966dc565f3de95878cf7a12 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 11:37:28 +0100 Subject: [PATCH 136/184] initial attempt at unit test --- .../TextBoxTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 64c0020f92..0031d005e7 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -556,6 +556,37 @@ namespace Avalonia.Controls.UnitTests Assert.True(true); } } + + [Fact] + public void Textbox_Cannot_Focus_When_not_Visible() + { + using (UnitTestApplication.Start(FocusServices)) + { + var target1 = new TextBox + { + Template = CreateTemplate(), + Text = "1234", + IsVisible = true + }; + + target1.ApplyTemplate(); + + var root = new TestRoot { Child = target1 }; + + var gfcount = 0; + var lfcount = 0; + + target1.GotFocus += (s, e) => gfcount++; + + target1.Focus(); + Assert.True(target1.IsFocused); + + RaiseKeyEvent(target1, Key.Up, KeyModifiers.None); + + Assert.Equal(1, gfcount); + Assert.Equal(1, lfcount); + } + } [Fact] public void TextBox_GotFocus_And_LostFocus_Work_Properly() From 73aaefcc32d9c9853ffaf5daf88606e09e81bdb6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 12:29:59 +0100 Subject: [PATCH 137/184] add a failing unit test to demonstrate the issue. --- .../TextBoxTests.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 0031d005e7..dec9daa73e 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; @@ -558,7 +559,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Textbox_Cannot_Focus_When_not_Visible() + public void Textbox_doesnt_crash_when_Receives_input_and_hidden() { using (UnitTestApplication.Start(FocusServices)) { @@ -566,25 +567,17 @@ namespace Avalonia.Controls.UnitTests { Template = CreateTemplate(), Text = "1234", - IsVisible = true + IsVisible = false }; - target1.ApplyTemplate(); - var root = new TestRoot { Child = target1 }; - var gfcount = 0; - var lfcount = 0; - - target1.GotFocus += (s, e) => gfcount++; + root.Measure(new Size(1000, 1000)); target1.Focus(); Assert.True(target1.IsFocused); RaiseKeyEvent(target1, Key.Up, KeyModifiers.None); - - Assert.Equal(1, gfcount); - Assert.Equal(1, lfcount); } } @@ -794,6 +787,9 @@ namespace Avalonia.Controls.UnitTests keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + textShaperImpl: new MockTextShaperImpl(), standardCursorFactory: Mock.Of()); private static TestServices Services => TestServices.MockThreadingInterface.With( From a295cac93050cb3020225702472a8243d892005a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 12:33:13 +0100 Subject: [PATCH 138/184] test more keys. --- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index dec9daa73e..3e71c38335 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -558,8 +558,12 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Textbox_doesnt_crash_when_Receives_input_and_hidden() + [Theory] + [InlineData(Key.Up)] + [InlineData(Key.Down)] + [InlineData(Key.Home)] + [InlineData(Key.End)] + public void Textbox_doesnt_crash_when_Receives_input_and_hidden(Key key) { using (UnitTestApplication.Start(FocusServices)) { @@ -577,7 +581,7 @@ namespace Avalonia.Controls.UnitTests target1.Focus(); Assert.True(target1.IsFocused); - RaiseKeyEvent(target1, Key.Up, KeyModifiers.None); + RaiseKeyEvent(target1, key, KeyModifiers.None); } } From d0133ebb8cf485e54a43e0cc7aeebeb34b9db8b3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 12:35:09 +0100 Subject: [PATCH 139/184] check we have a presenter before allowing navigation inside textbox. --- src/Avalonia.Controls/TextBox.cs | 45 ++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index c1516613b3..40c075c7af 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1098,29 +1098,35 @@ namespace Avalonia.Controls private bool MoveVertical(int count) { - var formattedText = _presenter.FormattedText; - var lines = formattedText.GetLines().ToList(); - var caretIndex = CaretIndex; - var lineIndex = GetLine(caretIndex, lines) + count; - - if (lineIndex >= 0 && lineIndex < lines.Count) - { - var line = lines[lineIndex]; - var rect = formattedText.HitTestTextPosition(caretIndex); - var y = count < 0 ? rect.Y : rect.Bottom; - var point = new Point(rect.X, y + (count * (line.Height / 2))); - var hit = formattedText.HitTestPoint(point); - CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); - return true; - } - else + if (_presenter != null) { - return false; + var formattedText = _presenter.FormattedText; + var lines = formattedText.GetLines().ToList(); + var caretIndex = CaretIndex; + var lineIndex = GetLine(caretIndex, lines) + count; + + if (lineIndex >= 0 && lineIndex < lines.Count) + { + var line = lines[lineIndex]; + var rect = formattedText.HitTestTextPosition(caretIndex); + var y = count < 0 ? rect.Y : rect.Bottom; + var point = new Point(rect.X, y + (count * (line.Height / 2))); + var hit = formattedText.HitTestPoint(point); + CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); + return true; + } } + + return false; } private void MoveHome(bool document) { + if (_presenter == null) + { + return; + } + var text = Text ?? string.Empty; var caretIndex = CaretIndex; @@ -1151,6 +1157,11 @@ namespace Avalonia.Controls private void MoveEnd(bool document) { + if (_presenter == null) + { + return; + } + var text = Text ?? string.Empty; var caretIndex = CaretIndex; From b250fce59c1c51724c57046c3ed51d059adabf6b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 12:56:16 +0100 Subject: [PATCH 140/184] rename test to describe the actual issue. --- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 3e71c38335..cb20071860 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -563,7 +563,7 @@ namespace Avalonia.Controls.UnitTests [InlineData(Key.Down)] [InlineData(Key.Home)] [InlineData(Key.End)] - public void Textbox_doesnt_crash_when_Receives_input_and_hidden(Key key) + public void Textbox_doesnt_crash_when_Receives_input_and_template_not_applied(Key key) { using (UnitTestApplication.Start(FocusServices)) { @@ -576,8 +576,6 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = target1 }; - root.Measure(new Size(1000, 1000)); - target1.Focus(); Assert.True(target1.IsFocused); From dc3b323dc1b0852f0d071b9fd284b5e5563e35fe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 12:56:41 +0100 Subject: [PATCH 141/184] refactor a little. --- src/Avalonia.Controls/TextBox.cs | 36 +++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 40c075c7af..3aef2abac5 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1098,23 +1098,25 @@ namespace Avalonia.Controls private bool MoveVertical(int count) { - if (_presenter != null) + if (_presenter is null) { - var formattedText = _presenter.FormattedText; - var lines = formattedText.GetLines().ToList(); - var caretIndex = CaretIndex; - var lineIndex = GetLine(caretIndex, lines) + count; + return false; + } - if (lineIndex >= 0 && lineIndex < lines.Count) - { - var line = lines[lineIndex]; - var rect = formattedText.HitTestTextPosition(caretIndex); - var y = count < 0 ? rect.Y : rect.Bottom; - var point = new Point(rect.X, y + (count * (line.Height / 2))); - var hit = formattedText.HitTestPoint(point); - CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); - return true; - } + var formattedText = _presenter.FormattedText; + var lines = formattedText.GetLines().ToList(); + var caretIndex = CaretIndex; + var lineIndex = GetLine(caretIndex, lines) + count; + + if (lineIndex >= 0 && lineIndex < lines.Count) + { + var line = lines[lineIndex]; + var rect = formattedText.HitTestTextPosition(caretIndex); + var y = count < 0 ? rect.Y : rect.Bottom; + var point = new Point(rect.X, y + (count * (line.Height / 2))); + var hit = formattedText.HitTestPoint(point); + CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); + return true; } return false; @@ -1122,7 +1124,7 @@ namespace Avalonia.Controls private void MoveHome(bool document) { - if (_presenter == null) + if (_presenter is null) { return; } @@ -1157,7 +1159,7 @@ namespace Avalonia.Controls private void MoveEnd(bool document) { - if (_presenter == null) + if (_presenter is null) { return; } From 975e48d3a2afb16d072a1f78c9735e5e2cb4d3bd Mon Sep 17 00:00:00 2001 From: Royce551 Date: Thu, 8 Jul 2021 08:11:27 -0500 Subject: [PATCH 142/184] Move to SelectingItemsControl --- src/Avalonia.Controls/ItemsControl.cs | 74 ------------------- .../Primitives/SelectingItemsControl.cs | 74 +++++++++++++++++++ 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index c86f2e591b..55645d4dbb 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -13,7 +13,6 @@ using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; -using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -54,14 +53,6 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); - /// - /// Defines the property. - /// - public static readonly StyledProperty IsTextSearchEnabledProperty = - AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); - - private string _textSearchTerm = string.Empty; - private DispatcherTimer _textSearchTimer; private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; @@ -145,15 +136,6 @@ namespace Avalonia.Controls set { SetValue(ItemTemplateProperty, value); } } - /// - /// Gets or sets a value that specifies whether a user can jump to a value by typing. - /// - public bool IsTextSearchEnabled - { - get { return GetValue(IsTextSearchEnabledProperty); } - set { SetValue(IsTextSearchEnabledProperty, value); } - } - /// /// Gets the items presenter control. /// @@ -342,36 +324,6 @@ namespace Avalonia.Controls base.OnKeyDown(e); } - protected override void OnTextInput(TextInputEventArgs e) - { - if (!e.Handled && this is SelectingItemsControl selectingItemsControl) - { - if (!IsTextSearchEnabled) - return; - - StopTextSearchTimer(); - - _textSearchTerm += e.Text; - - bool match(ItemContainerInfo info) => - info.ContainerControl is IContentControl control && - control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; - - var info = ItemContainerGenerator.Containers.FirstOrDefault(match); - - if (info != null) - { - selectingItemsControl.SelectedIndex = info.Index; - } - - StartTextSearchTimer(); - - e.Handled = true; - } - - base.OnTextInput(e); - } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -505,32 +457,6 @@ namespace Avalonia.Controls } } - private void StartTextSearchTimer() - { - _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; - _textSearchTimer.Tick += TextSearchTimer_Tick; - _textSearchTimer.Start(); - } - - private void StopTextSearchTimer() - { - if (_textSearchTimer == null) - { - return; - } - - _textSearchTimer.Stop(); - _textSearchTimer.Tick -= TextSearchTimer_Tick; - - _textSearchTimer = null; - } - - private void TextSearchTimer_Tick(object sender, EventArgs e) - { - _textSearchTerm = string.Empty; - StopTextSearchTimer(); - } - private void UpdateItemCount() { if (Items == null) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 34d3347434..1207b7f2dc 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -10,6 +10,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.Threading; using Avalonia.VisualTree; #nullable enable @@ -91,6 +92,12 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register( nameof(SelectionMode)); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsTextSearchEnabledProperty = + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); + /// /// Event that should be raised by items that implement to /// notify the parent that their selection state @@ -110,6 +117,8 @@ namespace Avalonia.Controls.Primitives RoutingStrategies.Bubble); private static readonly IList Empty = Array.Empty(); + private string _textSearchTerm = string.Empty; + private DispatcherTimer? _textSearchTimer; private ISelectionModel? _selection; private int _oldSelectedIndex; private object? _oldSelectedItem; @@ -305,6 +314,15 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Gets or sets a value that specifies whether a user can jump to a value by typing. + /// + public bool IsTextSearchEnabled + { + get { return GetValue(IsTextSearchEnabledProperty); } + set { SetValue(IsTextSearchEnabledProperty, value); } + } + /// /// Gets or sets the selection mode. /// @@ -490,6 +508,36 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnTextInput(TextInputEventArgs e) + { + if (!e.Handled) + { + if (!IsTextSearchEnabled) + return; + + StopTextSearchTimer(); + + _textSearchTerm += e.Text; + + bool match(ItemContainerInfo info) => + info.ContainerControl is IContentControl control && + control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + + var info = ItemContainerGenerator.Containers.FirstOrDefault(match); + + if (info != null) + { + SelectedIndex = info.Index; + } + + StartTextSearchTimer(); + + e.Handled = true; + } + + base.OnTextInput(e); + } + protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); @@ -962,6 +1010,32 @@ namespace Avalonia.Controls.Primitives } } + private void StartTextSearchTimer() + { + _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _textSearchTimer.Tick += TextSearchTimer_Tick; + _textSearchTimer.Start(); + } + + private void StopTextSearchTimer() + { + if (_textSearchTimer == null) + { + return; + } + + _textSearchTimer.Stop(); + _textSearchTimer.Tick -= TextSearchTimer_Tick; + + _textSearchTimer = null; + } + + private void TextSearchTimer_Tick(object sender, EventArgs e) + { + _textSearchTerm = string.Empty; + StopTextSearchTimer(); + } + // When in a BeginInit..EndInit block, or when the DataContext is updating, we need to // defer changes to the selection model because we have no idea in which order properties // will be set. Consider: From f053bfef3a6f948053ebe495e6eac6a0fdc1e9b3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 14:18:47 +0100 Subject: [PATCH 143/184] macro and add self pointer retaining to all COM calls. --- native/Avalonia.Native/inc/comimpl.h | 1 + native/Avalonia.Native/src/OSX/AvnString.mm | 26 ++- native/Avalonia.Native/src/OSX/Screens.mm | 4 + native/Avalonia.Native/src/OSX/cgl.mm | 8 + native/Avalonia.Native/src/OSX/clipboard.mm | 82 ++++--- native/Avalonia.Native/src/OSX/controlhost.mm | 63 ++++-- native/Avalonia.Native/src/OSX/cursor.mm | 52 +++-- native/Avalonia.Native/src/OSX/main.mm | 174 +++++++++++---- native/Avalonia.Native/src/OSX/menu.mm | 22 ++ .../src/OSX/platformthreading.mm | 2 + .../Avalonia.Native/src/OSX/rendertarget.mm | 6 + native/Avalonia.Native/src/OSX/window.mm | 207 +++++++++++++----- 12 files changed, 461 insertions(+), 186 deletions(-) diff --git a/native/Avalonia.Native/inc/comimpl.h b/native/Avalonia.Native/inc/comimpl.h index c9f70cfa3b..45a8e8690d 100644 --- a/native/Avalonia.Native/inc/comimpl.h +++ b/native/Avalonia.Native/inc/comimpl.h @@ -7,6 +7,7 @@ #define COMIMPL_H_INCLUDED #include +#define START_COM_CALL auto r = this->UnknownSelf() __IID_DEF(IUnknown, 0, 0, 0, C0, 00, 00, 00, 00, 00, 00, 46); diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index 001cf151d8..6d057fb705 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -43,6 +43,8 @@ public: virtual HRESULT Pointer(void**retOut) override { + START_COM_CALL; + @autoreleasepool { if(retOut == nullptr) @@ -58,14 +60,13 @@ public: virtual HRESULT Length(int*retOut) override { - if(retOut == nullptr) + START_COM_CALL; + + @autoreleasepool { - return E_POINTER; + i@autoreleasepool + {eturn S_OK; } - - *retOut = _length; - - return S_OK; } }; @@ -109,10 +110,15 @@ public: virtual HRESULT Get(unsigned int index, IAvnString**ppv) override { - if(_list.size() <= index) - return E_INVALIDARG; - *ppv = _list[index].getRetainedReference(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + if(_list.size() <= index) + return E_INVALIDARG; + *ppv = _list[index].getRetainedReference(); + return S_OK; + } } }; diff --git a/native/Avalonia.Native/src/OSX/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index 10f698ff45..b9c75ed742 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -8,6 +8,8 @@ class Screens : public ComSingleObject public: virtual HRESULT GetScreenCount (int* ret) override { + START_COM_CALL; + @autoreleasepool { *ret = (int)[NSScreen screens].count; @@ -18,6 +20,8 @@ public: virtual HRESULT GetScreen (int index, AvnScreen* ret) override { + START_COM_CALL; + @autoreleasepool { if(index < 0 || index >= [NSScreen screens].count) diff --git a/native/Avalonia.Native/src/OSX/cgl.mm b/native/Avalonia.Native/src/OSX/cgl.mm index a9d94cdf04..085037978e 100644 --- a/native/Avalonia.Native/src/OSX/cgl.mm +++ b/native/Avalonia.Native/src/OSX/cgl.mm @@ -69,6 +69,8 @@ public: virtual HRESULT LegacyMakeCurrent() override { + START_COM_CALL; + if(CGLSetCurrentContext(Context) != 0) return E_FAIL; return S_OK; @@ -76,6 +78,8 @@ public: virtual HRESULT MakeCurrent(IUnknown** ppv) override { + START_COM_CALL; + CGLContextObj saved = CGLGetCurrentContext(); CGLLockContext(Context); if(CGLSetCurrentContext(Context) != 0) @@ -128,6 +132,8 @@ public: virtual HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv) override { + START_COM_CALL; + CGLContextObj shareContext = nil; if(share != nil) { @@ -144,6 +150,8 @@ public: virtual HRESULT WrapContext(void* native, IAvnGlContext**ppv) override { + START_COM_CALL; + if(native == nil) return E_INVALIDARG; *ppv = new AvnGlContext((CGLContextObj) native); diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index f148374759..9966971b73 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -25,6 +25,8 @@ public: virtual HRESULT GetText (char* type, IAvnString**ppv) override { + START_COM_CALL; + @autoreleasepool { if(ppv == nullptr) @@ -42,6 +44,8 @@ public: virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override { + START_COM_CALL; + @autoreleasepool { *ppv= nil; @@ -69,56 +73,71 @@ public: virtual HRESULT SetText (char* type, char* utf8String) override { - Clear(); + START_COM_CALL; + @autoreleasepool { + Clear(); + auto string = [NSString stringWithUTF8String:(const char*)utf8String]; auto typeString = [NSString stringWithUTF8String:(const char*)type]; if(_item == nil) [_pb setString: string forType: typeString]; else [_item setString: string forType:typeString]; - } - return S_OK; + return S_OK; + } } virtual HRESULT SetBytes(char* type, void* bytes, int len) override { - auto typeString = [NSString stringWithUTF8String:(const char*)type]; - auto data = [NSData dataWithBytes:bytes length:len]; - if(_item == nil) - [_pb setData:data forType:typeString]; - else - [_item setData:data forType:typeString]; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + auto data = [NSData dataWithBytes:bytes length:len]; + if(_item == nil) + [_pb setData:data forType:typeString]; + else + [_item setData:data forType:typeString]; + return S_OK; + } } virtual HRESULT GetBytes(char* type, IAvnString**ppv) override { - *ppv = nil; - auto typeString = [NSString stringWithUTF8String:(const char*)type]; - NSData*data; - @try + START_COM_CALL; + + @autoreleasepool { - if(_item) - data = [_item dataForType:typeString]; - else - data = [_pb dataForType:typeString]; - if(data == nil) + *ppv = nil; + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + NSData*data; + @try + { + if(_item) + data = [_item dataForType:typeString]; + else + data = [_pb dataForType:typeString]; + if(data == nil) + return E_FAIL; + } + @catch(NSException* e) + { return E_FAIL; + } + *ppv = CreateByteArray((void*)data.bytes, (int)data.length); + return S_OK; } - @catch(NSException* e) - { - return E_FAIL; - } - *ppv = CreateByteArray((void*)data.bytes, (int)data.length); - return S_OK; } virtual HRESULT Clear() override { + START_COM_CALL; + @autoreleasepool { if(_item != nil) @@ -128,15 +147,20 @@ public: [_pb clearContents]; [_pb setString:@"" forType:NSPasteboardTypeString]; } - } - return S_OK; + return S_OK; + } } virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override { - *ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); + return S_OK; + } } }; diff --git a/native/Avalonia.Native/src/OSX/controlhost.mm b/native/Avalonia.Native/src/OSX/controlhost.mm index 5ee2344ac7..f8e9a3b6d1 100644 --- a/native/Avalonia.Native/src/OSX/controlhost.mm +++ b/native/Avalonia.Native/src/OSX/controlhost.mm @@ -16,11 +16,16 @@ public: virtual HRESULT CreateDefaultChild(void* parent, void** retOut) override { - NSView* view = [NSView new]; - [view setWantsLayer: true]; + START_COM_CALL; - *retOut = (__bridge_retained void*)view; - return S_OK; + @autoreleasepool + { + NSView* view = [NSView new]; + [view setWantsLayer: true]; + + *retOut = (__bridge_retained void*)view; + return S_OK; + } }; virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() override @@ -69,32 +74,42 @@ public: virtual HRESULT InitializeWithChildHandle(void* child) override { - if(_child != nil) - return E_FAIL; - _child = (__bridge NSView*)child; - if(_child == nil) - return E_FAIL; - [_holder addSubview:_child]; - [_child setHidden: false]; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + if(_child != nil) + return E_FAIL; + _child = (__bridge NSView*)child; + if(_child == nil) + return E_FAIL; + [_holder addSubview:_child]; + [_child setHidden: false]; + return S_OK; + } }; virtual HRESULT AttachTo(IAvnNativeControlHost* host) override { - if(host == nil) - { - [_holder removeFromSuperview]; - [_holder setHidden: true]; - } - else + START_COM_CALL; + + @autoreleasepool { - AvnNativeControlHost* chost = dynamic_cast(host); - if(chost == nil || chost->View == nil) - return E_FAIL; - [_holder setHidden:true]; - [chost->View addSubview:_holder]; + if(host == nil) + { + [_holder removeFromSuperview]; + [_holder setHidden: true]; + } + else + { + AvnNativeControlHost* chost = dynamic_cast(host); + if(chost == nil || chost->View == nil) + return E_FAIL; + [_holder setHidden:true]; + [chost->View addSubview:_holder]; + } + return S_OK; } - return S_OK; }; virtual void ShowInBounds(float x, float y, float width, float height) override diff --git a/native/Avalonia.Native/src/OSX/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm index 1732d6e71f..dc38294a18 100644 --- a/native/Avalonia.Native/src/OSX/cursor.mm +++ b/native/Avalonia.Native/src/OSX/cursor.mm @@ -53,36 +53,46 @@ public: virtual HRESULT GetCursor (AvnStandardCursorType cursorType, IAvnCursor** retOut) override { - *retOut = s_cursorMap[cursorType]; + START_COM_CALL; - if(*retOut != nullptr) + @autoreleasepool { - (*retOut)->AddRef(); - } + *retOut = s_cursorMap[cursorType]; - return S_OK; + if(*retOut != nullptr) + { + (*retOut)->AddRef(); + } + + return S_OK; + } } virtual HRESULT CreateCustomCursor (void* bitmapData, size_t length, AvnPixelSize hotPixel, IAvnCursor** retOut) override { - if(bitmapData == nullptr || retOut == nullptr) + START_COM_CALL; + + @autoreleasepool { - return E_POINTER; + if(bitmapData == nullptr || retOut == nullptr) + { + return E_POINTER; + } + + NSData *imageData = [NSData dataWithBytes:bitmapData length:length]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + + NSPoint hotSpot; + hotSpot.x = hotPixel.Width; + hotSpot.y = hotPixel.Height; + + *retOut = new Cursor([[NSCursor new] initWithImage: image hotSpot: hotSpot]); + + (*retOut)->AddRef(); + + return S_OK; } - - NSData *imageData = [NSData dataWithBytes:bitmapData length:length]; - NSImage *image = [[NSImage alloc] initWithData:imageData]; - - - NSPoint hotSpot; - hotSpot.x = hotPixel.Width; - hotSpot.y = hotPixel.Height; - - *retOut = new Cursor([[NSCursor new] initWithImage: image hotSpot: hotSpot]); - - (*retOut)->AddRef(); - - return S_OK; } }; diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index aaaf381b26..3e152a6125 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -107,27 +107,42 @@ public: virtual HRESULT SetApplicationTitle(char* utf8String) override { - auto appTitle = [NSString stringWithUTF8String: utf8String]; + START_COM_CALL; - [[NSProcessInfo processInfo] setProcessName:appTitle]; - - - SetProcessName(appTitle); - - return S_OK; + @autoreleasepool + { + auto appTitle = [NSString stringWithUTF8String: utf8String]; + + [[NSProcessInfo processInfo] setProcessName:appTitle]; + + + SetProcessName(appTitle); + + return S_OK; + } } virtual HRESULT SetShowInDock(int show) override { - AvnDesiredActivationPolicy = show - ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + AvnDesiredActivationPolicy = show + ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; + return S_OK; + } } virtual HRESULT SetDisableDefaultApplicationMenuItems (bool enabled) override { - SetAutoGenerateDefaultAppMenuItems(!enabled); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + SetAutoGenerateDefaultAppMenuItems(!enabled); + return S_OK; + } } }; @@ -165,6 +180,8 @@ public: FORWARD_IUNKNOWN() virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* events) override { + START_COM_CALL; + _deallocator = deallocator; @autoreleasepool{ [[ThreadingInitializer new] do]; @@ -180,89 +197,154 @@ public: virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) override { - if(cb == nullptr || ppv == nullptr) - return E_POINTER; - *ppv = CreateAvnWindow(cb, gl); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + if(cb == nullptr || ppv == nullptr) + return E_POINTER; + *ppv = CreateAvnWindow(cb, gl); + return S_OK; + } }; virtual HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) override { - if(cb == nullptr || ppv == nullptr) - return E_POINTER; + START_COM_CALL; - *ppv = CreateAvnPopup(cb, gl); - return S_OK; + @autoreleasepool + { + if(cb == nullptr || ppv == nullptr) + return E_POINTER; + + *ppv = CreateAvnPopup(cb, gl); + return S_OK; + } } virtual HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface** ppv) override { - *ppv = CreatePlatformThreading(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = CreatePlatformThreading(); + return S_OK; + } } virtual HRESULT CreateSystemDialogs(IAvnSystemDialogs** ppv) override { - *ppv = ::CreateSystemDialogs(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateSystemDialogs(); + return S_OK; + } } virtual HRESULT CreateScreens (IAvnScreens** ppv) override { - *ppv = ::CreateScreens (); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateScreens (); + return S_OK; + } } virtual HRESULT CreateClipboard(IAvnClipboard** ppv) override { - *ppv = ::CreateClipboard (nil, nil); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateClipboard (nil, nil); + return S_OK; + } } virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) override { - *ppv = ::CreateClipboard (nil, [NSPasteboardItem new]); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateClipboard (nil, [NSPasteboardItem new]); + return S_OK; + } } virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) override { - *ppv = ::CreateCursorFactory(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateCursorFactory(); + return S_OK; + } } virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) override { - auto rv = ::GetGlDisplay(); - if(rv == NULL) - return E_FAIL; - rv->AddRef(); - *ppv = rv; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + auto rv = ::GetGlDisplay(); + if(rv == NULL) + return E_FAIL; + rv->AddRef(); + *ppv = rv; + return S_OK; + } } virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override { - *ppv = ::CreateAppMenu(cb); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateAppMenu(cb); + return S_OK; + } } virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) override { - *ppv = ::CreateAppMenuItem(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateAppMenuItem(); + return S_OK; + } } virtual HRESULT CreateMenuItemSeparator (IAvnMenuItem** ppv) override { - *ppv = ::CreateAppMenuItemSeparator(); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateAppMenuItemSeparator(); + return S_OK; + } } virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override { - ::SetAppMenu(s_appTitle, appMenu); - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + ::SetAppMenu(s_appTitle, appMenu); + return S_OK; + } } }; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index b9a95e7b3c..38f8c2a7cb 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -95,6 +95,8 @@ NSMenuItem* AvnAppMenuItem::GetNative() HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu) { + START_COM_CALL; + @autoreleasepool { if(menu != nullptr) @@ -114,6 +116,8 @@ HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu) HRESULT AvnAppMenuItem::SetTitle (char* utf8String) { + START_COM_CALL; + @autoreleasepool { if (utf8String != nullptr) @@ -128,6 +132,8 @@ HRESULT AvnAppMenuItem::SetTitle (char* utf8String) HRESULT AvnAppMenuItem::SetGesture (AvnKey key, AvnInputModifiers modifiers) { + START_COM_CALL; + @autoreleasepool { if(key != AvnKeyNone) @@ -183,6 +189,8 @@ HRESULT AvnAppMenuItem::SetGesture (AvnKey key, AvnInputModifiers modifiers) HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) { + START_COM_CALL; + @autoreleasepool { _predicate = predicate; @@ -193,6 +201,8 @@ HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionC HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) { + START_COM_CALL; + @autoreleasepool { [_native setState:(isChecked && _isCheckable ? NSOnState : NSOffState)]; @@ -202,6 +212,8 @@ HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) { + START_COM_CALL; + @autoreleasepool { switch(toggleType) @@ -231,6 +243,8 @@ HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) HRESULT AvnAppMenuItem::SetIcon(void *data, size_t length) { + START_COM_CALL; + @autoreleasepool { if(data != nullptr) @@ -317,6 +331,8 @@ void AvnAppMenu::RaiseClosed() HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { + START_COM_CALL; + @autoreleasepool { if([_native hasGlobalMenuItem]) @@ -337,6 +353,8 @@ HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) HRESULT AvnAppMenu::RemoveItem (IAvnMenuItem* item) { + START_COM_CALL; + @autoreleasepool { auto avnMenuItem = dynamic_cast(item); @@ -352,6 +370,8 @@ HRESULT AvnAppMenu::RemoveItem (IAvnMenuItem* item) HRESULT AvnAppMenu::SetTitle (char* utf8String) { + START_COM_CALL; + @autoreleasepool { if (utf8String != nullptr) @@ -365,6 +385,8 @@ HRESULT AvnAppMenu::SetTitle (char* utf8String) HRESULT AvnAppMenu::Clear() { + START_COM_CALL; + @autoreleasepool { [_native removeAllItems]; diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index e83bf53331..6d5bd4aa02 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -114,6 +114,8 @@ public: virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) override { + START_COM_CALL; + auto can = dynamic_cast(cancel); if(can->Cancelled) return S_OK; diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm index b2d4341bb9..dc5c24e41e 100644 --- a/native/Avalonia.Native/src/OSX/rendertarget.mm +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -247,6 +247,8 @@ public: virtual HRESULT GetPixelSize(AvnPixelSize* ret) override { + START_COM_CALL; + if(!_surface) return E_FAIL; *ret = _surface->size; @@ -255,6 +257,8 @@ public: virtual HRESULT GetScaling(double* ret) override { + START_COM_CALL; + if(!_surface) return E_FAIL; *ret = _surface->scale; @@ -281,6 +285,8 @@ public: virtual HRESULT BeginDrawing(IAvnGlSurfaceRenderingSession** ret) override { + START_COM_CALL; + ComPtr releaseContext; @synchronized (_target->lock) { if(_target->surface == nil) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 439f6710b9..a163106d2d 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -54,6 +54,8 @@ public: virtual HRESULT ObtainNSWindowHandle(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -66,6 +68,8 @@ public: virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -78,6 +82,8 @@ public: virtual HRESULT ObtainNSViewHandle(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -90,6 +96,8 @@ public: virtual HRESULT ObtainNSViewHandleRetained(void** ret) override { + START_COM_CALL; + if (ret == nullptr) { return E_POINTER; @@ -107,6 +115,8 @@ public: virtual HRESULT Show(bool activate) override { + START_COM_CALL; + @autoreleasepool { SetPosition(lastPositionSet); @@ -139,6 +149,8 @@ public: virtual HRESULT Hide () override { + START_COM_CALL; + @autoreleasepool { if(Window != nullptr) @@ -153,6 +165,8 @@ public: virtual HRESULT Activate () override { + START_COM_CALL; + @autoreleasepool { if(Window != nullptr) @@ -167,6 +181,8 @@ public: virtual HRESULT SetTopMost (bool value) override { + START_COM_CALL; + @autoreleasepool { [Window setLevel: value ? NSFloatingWindowLevel : NSNormalWindowLevel]; @@ -177,6 +193,8 @@ public: virtual HRESULT Close() override { + START_COM_CALL; + @autoreleasepool { if (Window != nullptr) @@ -193,6 +211,8 @@ public: virtual HRESULT GetClientSize(AvnSize* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -208,6 +228,8 @@ public: virtual HRESULT GetScaling (double* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -226,6 +248,8 @@ public: virtual HRESULT SetMinMaxSize (AvnSize minSize, AvnSize maxSize) override { + START_COM_CALL; + @autoreleasepool { [Window setMinSize: ToNSSize(minSize)]; @@ -237,6 +261,8 @@ public: virtual HRESULT Resize(double x, double y) override { + START_COM_CALL; + @autoreleasepool { auto maxSize = [Window maxSize]; @@ -276,6 +302,8 @@ public: virtual HRESULT Invalidate (AvnRect rect) override { + START_COM_CALL; + @autoreleasepool { [View setNeedsDisplayInRect:[View frame]]; @@ -286,6 +314,8 @@ public: virtual HRESULT SetMainMenu(IAvnMenu* menu) override { + START_COM_CALL; + _mainMenu = menu; auto nativeMenu = dynamic_cast(menu); @@ -304,6 +334,8 @@ public: virtual HRESULT BeginMoveDrag () override { + START_COM_CALL; + @autoreleasepool { auto lastEvent = [View lastMouseDownEvent]; @@ -321,11 +353,15 @@ public: virtual HRESULT BeginResizeDrag (AvnWindowEdge edge) override { + START_COM_CALL; + return S_OK; } virtual HRESULT GetPosition (AvnPoint* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -346,6 +382,8 @@ public: virtual HRESULT SetPosition (AvnPoint point) override { + START_COM_CALL; + @autoreleasepool { lastPositionSet = point; @@ -357,6 +395,8 @@ public: virtual HRESULT PointToClient (AvnPoint point, AvnPoint* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -375,6 +415,8 @@ public: virtual HRESULT PointToScreen (AvnPoint point, AvnPoint* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -392,12 +434,16 @@ public: virtual HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer* fb, IUnknown* dispose) override { + START_COM_CALL; + [View setSwRenderedFrame: fb dispose: dispose]; return S_OK; } virtual HRESULT SetCursor(IAvnCursor* cursor) override { + START_COM_CALL; + @autoreleasepool { Cursor* avnCursor = dynamic_cast(cursor); @@ -427,6 +473,8 @@ public: virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ppv) override { + START_COM_CALL; + if(View == NULL) return E_FAIL; *ppv = [renderTarget createSurfaceRenderTarget]; @@ -435,6 +483,8 @@ public: virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override { + START_COM_CALL; + if(View == NULL) return E_FAIL; *retOut = ::CreateNativeControlHost(View); @@ -443,6 +493,8 @@ public: virtual HRESULT SetBlurEnabled (bool enable) override { + START_COM_CALL; + [StandardContainer ShowBlur:enable]; return S_OK; @@ -452,6 +504,8 @@ public: IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) override { + START_COM_CALL; + auto item = TryGetPasteboardItem(clipboard); [item setString:@"" forType:GetAvnCustomDataType()]; if(item == nil) @@ -584,10 +638,10 @@ private: virtual HRESULT Show (bool activate) override { + START_COM_CALL; + @autoreleasepool { - auto r = this->UnknownSelf(); - WindowBaseImpl::Show(activate); HideOrShowTrafficLights(); @@ -598,6 +652,8 @@ private: virtual HRESULT SetEnabled (bool enable) override { + START_COM_CALL; + @autoreleasepool { [Window setEnabled:enable]; @@ -607,6 +663,8 @@ private: virtual HRESULT SetParent (IAvnWindow* parent) override { + START_COM_CALL; + @autoreleasepool { if(parent == nullptr) @@ -718,6 +776,8 @@ private: virtual HRESULT SetCanResize(bool value) override { + START_COM_CALL; + @autoreleasepool { _canResize = value; @@ -728,6 +788,8 @@ private: virtual HRESULT SetDecorations(SystemDecorations value) override { + START_COM_CALL; + @autoreleasepool { auto currentWindowState = _lastWindowState; @@ -793,6 +855,8 @@ private: virtual HRESULT SetTitle (char* utf8title) override { + START_COM_CALL; + @autoreleasepool { _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; @@ -804,6 +868,8 @@ private: virtual HRESULT SetTitleBarColor(AvnColor color) override { + START_COM_CALL; + @autoreleasepool { float a = (float)color.Alpha / 255.0f; @@ -833,6 +899,8 @@ private: virtual HRESULT GetWindowState (AvnWindowState*ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -866,86 +934,111 @@ private: virtual HRESULT TakeFocusFromChildren () override { - if(Window == nil) - return S_OK; - if([Window isKeyWindow]) - [Window makeFirstResponder: View]; + START_COM_CALL; - return S_OK; + @autoreleasepool + { + if(Window == nil) + return S_OK; + if([Window isKeyWindow]) + [Window makeFirstResponder: View]; + + return S_OK; + } } virtual HRESULT SetExtendClientArea (bool enable) override { - _isClientAreaExtended = enable; + START_COM_CALL; - if(enable) + @autoreleasepool { - Window.titleVisibility = NSWindowTitleHidden; - - [Window setTitlebarAppearsTransparent:true]; - - auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - - if (wantsTitleBar) - { - [StandardContainer ShowTitleBar:true]; - } - else - { - [StandardContainer ShowTitleBar:false]; - } + _isClientAreaExtended = enable; - if(_extendClientHints & AvnOSXThickTitleBar) + if(enable) { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; + Window.titleVisibility = NSWindowTitleHidden; + + [Window setTitlebarAppearsTransparent:true]; + + auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + if (wantsTitleBar) + { + [StandardContainer ShowTitleBar:true]; + } + else + { + [StandardContainer ShowTitleBar:false]; + } + + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + else + { + Window.toolbar = nullptr; + } } else { + Window.titleVisibility = NSWindowTitleVisible; Window.toolbar = nullptr; + [Window setTitlebarAppearsTransparent:false]; + View.layer.zPosition = 0; } + + [Window setIsExtended:enable]; + + HideOrShowTrafficLights(); + + UpdateStyle(); + + return S_OK; } - else - { - Window.titleVisibility = NSWindowTitleVisible; - Window.toolbar = nullptr; - [Window setTitlebarAppearsTransparent:false]; - View.layer.zPosition = 0; - } - - [Window setIsExtended:enable]; - - HideOrShowTrafficLights(); - - UpdateStyle(); - - return S_OK; } virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override { - _extendClientHints = hints; + START_COM_CALL; - SetExtendClientArea(_isClientAreaExtended); - return S_OK; + @autoreleasepool + { + _extendClientHints = hints; + + SetExtendClientArea(_isClientAreaExtended); + return S_OK; + } } virtual HRESULT GetExtendTitleBarHeight (double*ret) override { - if(ret == nullptr) + START_COM_CALL; + + @autoreleasepool { - return E_POINTER; + if(ret == nullptr) + { + return E_POINTER; + } + + *ret = [Window getExtendedTitleBarHeight]; + + return S_OK; } - - *ret = [Window getExtendedTitleBarHeight]; - - return S_OK; } virtual HRESULT SetExtendTitleBarHeight (double value) override { - [StandardContainer SetTitleBarHeightHint:value]; - return S_OK; + START_COM_CALL; + + @autoreleasepool + { + [StandardContainer SetTitleBarHeightHint:value]; + return S_OK; + } } void EnterFullScreenMode () @@ -974,13 +1067,15 @@ private: virtual HRESULT SetWindowState (AvnWindowState state) override { - if(Window == nullptr) - { - return S_OK; - } + START_COM_CALL; @autoreleasepool { + if(Window == nullptr) + { + return S_OK; + } + if(_actualWindowState == state) { return S_OK; From 9da6a28b5161d11d346c6205fce9cbde1cc3629d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 14:21:18 +0100 Subject: [PATCH 144/184] fix --- native/Avalonia.Native/src/OSX/AvnString.mm | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index 6d057fb705..cd0e2cdf94 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -64,8 +64,14 @@ public: @autoreleasepool { - i@autoreleasepool - {eturn S_OK; + if(retOut == nullptr) + { + return E_POINTER; + } + + *retOut = _length; + + return S_OK; } } }; From 927fd90d860038f97c0581da5e1cc59132f0fa51 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 15:24:21 +0100 Subject: [PATCH 145/184] add documentation about START_COM_CALL. --- native/Avalonia.Native/inc/comimpl.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/native/Avalonia.Native/inc/comimpl.h b/native/Avalonia.Native/inc/comimpl.h index 45a8e8690d..d38216b49e 100644 --- a/native/Avalonia.Native/inc/comimpl.h +++ b/native/Avalonia.Native/inc/comimpl.h @@ -7,6 +7,17 @@ #define COMIMPL_H_INCLUDED #include + +/** + START_COM_CALL causes AddRef to be called at the beggining of a function. + When a function is exited, it causes ReleaseRef to be called. + This ensures that the object cannot be destructed whilst the function is running. + For example: Window Show is called, which triggers an event, and user calls Close inside the event + causing the refcount to reach 0, and the object to be destroyed. Function then continues and this pointer + will now be invalid. + + START_COM_CALL protects against this scenario. + */ #define START_COM_CALL auto r = this->UnknownSelf() __IID_DEF(IUnknown, 0, 0, 0, C0, 00, 00, 00, 00, 00, 00, 46); From bac825c9995b8a48636e0f1eda81c3b9383a2041 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 15:33:44 +0100 Subject: [PATCH 146/184] destroyed, not destructed. --- native/Avalonia.Native/inc/comimpl.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/inc/comimpl.h b/native/Avalonia.Native/inc/comimpl.h index d38216b49e..47b0a3c5f2 100644 --- a/native/Avalonia.Native/inc/comimpl.h +++ b/native/Avalonia.Native/inc/comimpl.h @@ -11,7 +11,7 @@ /** START_COM_CALL causes AddRef to be called at the beggining of a function. When a function is exited, it causes ReleaseRef to be called. - This ensures that the object cannot be destructed whilst the function is running. + This ensures that the object cannot be destroyed whilst the function is running. For example: Window Show is called, which triggers an event, and user calls Close inside the event causing the refcount to reach 0, and the object to be destroyed. Function then continues and this pointer will now be invalid. From 39394bceb6988571ceacd7f4fb85e142d41b5965 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 9 Jul 2021 09:29:58 +0200 Subject: [PATCH 147/184] Don't try to shutdown if e.Cancel already set. This allows users to add their own `ShutdownRequested` handlers which can override the lifetime's handler. --- .../ClassicDesktopStyleApplicationLifetime.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 2256f4cb54..65f1f0fff2 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -132,6 +132,9 @@ namespace Avalonia.Controls.ApplicationLifetimes private void ShutdownRequested(object sender, CancelEventArgs e) { + if (e.Cancel) + return; + // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their // owners. From 3b56b93ba1822149d495d560263679d7d14ab80e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 9 Jul 2021 13:42:46 +0200 Subject: [PATCH 148/184] Make the designer work in the sandbox project. --- samples/Sandbox/Program.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/Sandbox/Program.cs b/samples/Sandbox/Program.cs index 1e74105196..52321b46a9 100644 --- a/samples/Sandbox/Program.cs +++ b/samples/Sandbox/Program.cs @@ -4,12 +4,12 @@ namespace Sandbox { public class Program { - static void Main(string[] args) - { + static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - .LogToTrace() - .StartWithClassicDesktopLifetime(args); - } + .LogToTrace(); } } From 96ae9b19b30aa5fe3afe9467fd57acdd5e8e997b Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 9 Jul 2021 16:23:53 +0200 Subject: [PATCH 149/184] Expose EnsureCapacity() on AvaloniaList --- src/Avalonia.Base/Collections/AvaloniaList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 5681214222..85b10d8af9 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -633,7 +633,7 @@ namespace Avalonia.Collections /// Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList(); - private void EnsureCapacity(int capacity) + public void EnsureCapacity(int capacity) { // Adapted from List implementation. var currentCapacity = _inner.Capacity; From 0864f3c562f70f7d9900914d388714360e18819d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Marki=C4=8D?= Date: Sat, 10 Jul 2021 14:24:32 +0200 Subject: [PATCH 150/184] Makes TextBox.UndoRedoState struct readonly --- src/Avalonia.Controls/TextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index c1516613b3..ddf55808ce 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -145,7 +145,7 @@ namespace Avalonia.Controls (o, v) => o.UndoLimit = v, unsetValue: -1); - struct UndoRedoState : IEquatable + readonly struct UndoRedoState : IEquatable { public string Text { get; } public int CaretPosition { get; } From cd54a9255df1e6f66c13cdf7a839df83d7b104dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Marki=C4=8D?= Date: Sat, 10 Jul 2021 14:30:28 +0200 Subject: [PATCH 151/184] Adds CodeRush directory to GIT ignored files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 7d672c7755..abf7674560 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,9 @@ _NCrunch_*/ *.ncrunchsolution.user nCrunchTemp_* +# CodeRush +.cr/ + # Others sql/ *.Cache From a55a6ec1ff6ade0cc471f0fc4415c33a341f2a20 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Sat, 10 Jul 2021 15:05:18 +0200 Subject: [PATCH 152/184] Fix RightToLeft TextWrapping --- src/Avalonia.Visuals/Media/GlyphRun.cs | 2 +- .../TextFormatting/ShapedTextCharacters.cs | 87 +++++++++++++------ .../Media/TextFormatting/TextCharacters.cs | 9 +- .../Media/TextFormatting/TextFormatterImpl.cs | 86 ++++++++++++------ .../TextFormatting/TextFormatterTests.cs | 35 ++++++++ 5 files changed, 161 insertions(+), 58 deletions(-) diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 2b787462e4..234122f6f5 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -582,7 +582,7 @@ namespace Avalonia.Media { var cluster = _glyphClusters[i]; - var codepointIndex = cluster - _characters.Start; + var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster; var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index b304b19910..64befe2e5c 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -90,7 +90,9 @@ namespace Avalonia.Media.TextFormatting /// The split result. public SplitTextCharactersResult Split(int length) { - var glyphCount = GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length); + var glyphCount = GlyphRun.IsLeftToRight ? + GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length) : + GlyphRun.FindGlyphIndex(GlyphRun.Characters.End - length); if (GlyphRun.Characters.Length == length) { @@ -102,31 +104,64 @@ namespace Avalonia.Media.TextFormatting return new SplitTextCharactersResult(this, null); } - var firstGlyphRun = new GlyphRun( - Properties.Typeface.GlyphTypeface, - Properties.FontRenderingEmSize, - GlyphRun.GlyphIndices.Take(glyphCount), - GlyphRun.GlyphAdvances.Take(glyphCount), - GlyphRun.GlyphOffsets.Take(glyphCount), - GlyphRun.Characters.Take(length), - GlyphRun.GlyphClusters.Take(glyphCount), - GlyphRun.BiDiLevel); - - var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties); - - var secondGlyphRun = new GlyphRun( - Properties.Typeface.GlyphTypeface, - Properties.FontRenderingEmSize, - GlyphRun.GlyphIndices.Skip(glyphCount), - GlyphRun.GlyphAdvances.Skip(glyphCount), - GlyphRun.GlyphOffsets.Skip(glyphCount), - GlyphRun.Characters.Skip(length), - GlyphRun.GlyphClusters.Skip(glyphCount), - GlyphRun.BiDiLevel); - - var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties); - - return new SplitTextCharactersResult(firstTextRun, secondTextRun); + if (GlyphRun.IsLeftToRight) + { + var firstGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Take(glyphCount), + GlyphRun.GlyphAdvances.Take(glyphCount), + GlyphRun.GlyphOffsets.Take(glyphCount), + GlyphRun.Characters.Take(length), + GlyphRun.GlyphClusters.Take(glyphCount), + GlyphRun.BiDiLevel); + + var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties); + + var secondGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Skip(glyphCount), + GlyphRun.GlyphAdvances.Skip(glyphCount), + GlyphRun.GlyphOffsets.Skip(glyphCount), + GlyphRun.Characters.Skip(length), + GlyphRun.GlyphClusters.Skip(glyphCount), + GlyphRun.BiDiLevel); + + var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties); + + return new SplitTextCharactersResult(firstTextRun, secondTextRun); + } + else + { + var take = GlyphRun.GlyphIndices.Length - glyphCount; + + var firstGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Take(take), + GlyphRun.GlyphAdvances.Take(take), + GlyphRun.GlyphOffsets.Take(take), + GlyphRun.Characters.Skip(length), + GlyphRun.GlyphClusters.Take(take), + GlyphRun.BiDiLevel); + + var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties); + + var secondGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Skip(take), + GlyphRun.GlyphAdvances.Skip(take), + GlyphRun.GlyphOffsets.Skip(take), + GlyphRun.Characters.Take(length), + GlyphRun.GlyphClusters.Skip(take), + GlyphRun.BiDiLevel); + + var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties); + + return new SplitTextCharactersResult(secondTextRun,firstTextRun); + } } public readonly struct SplitTextCharactersResult diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index 0779716ec8..cfca8f9ab2 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -134,7 +134,7 @@ namespace Avalonia.Media.TextFormatting var isFallback = typeface != defaultTypeface; count = 0; - var script = Script.Common; + var script = Script.Unknown; var direction = BiDiClass.LeftToRight; var font = typeface.GlyphTypeface; @@ -161,7 +161,7 @@ namespace Avalonia.Media.TextFormatting if (currentScript != script) { - if (script == Script.Inherited || script == Script.Common) + if (script is Script.Unknown) { script = currentScript; } @@ -174,13 +174,16 @@ namespace Avalonia.Media.TextFormatting } } - if (currentScript != Script.Common && currentScript != Script.Inherited) + //Only handle non whitespace here + if (!currentGrapheme.FirstCodepoint.IsWhiteSpace) { + //Stop at the first glyph that is present in the default typeface. if (isFallback && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } + //Stop at the first missing glyph if (!font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 6533c34ba0..df63b00c25 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -70,34 +70,74 @@ namespace Avalonia.Media.TextFormatting { var glyphTypeface = glyphRun.GlyphTypeface; - for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) + if (glyphRun.IsLeftToRight) { - var glyph = glyphRun.GlyphIndices[i]; + foreach (var glyph in glyphRun.GlyphIndices) + { + var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; - var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; + if (currentWidth + advance > availableWidth) + { + break; + } - if (currentWidth + advance > availableWidth) - { - break; + currentWidth += advance; + + glyphCount++; } + } + else + { + for (var index = glyphRun.GlyphClusters.Length - 1; index > 0; index--) + { + var glyph = glyphRun.GlyphIndices[index]; + + var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; - currentWidth += advance; + if (currentWidth + advance > availableWidth) + { + break; + } - glyphCount++; + currentWidth += advance; + + glyphCount++; + } } } else { - foreach (var advance in glyphRun.GlyphAdvances) + if (glyphRun.IsLeftToRight) { - if (currentWidth + advance > availableWidth) + for (var index = 0; index < glyphRun.GlyphAdvances.Length; index++) { - break; + var advance = glyphRun.GlyphAdvances[index]; + + if (currentWidth + advance > availableWidth) + { + break; + } + + currentWidth += advance; + + glyphCount++; } + } + else + { + for (var index = glyphRun.GlyphAdvances.Length - 1; index > 0; index--) + { + var advance = glyphRun.GlyphAdvances[index]; + + if (currentWidth + advance > availableWidth) + { + break; + } - currentWidth += advance; + currentWidth += advance; - glyphCount++; + glyphCount++; + } } } @@ -475,24 +515,14 @@ namespace Avalonia.Media.TextFormatting var remainingCharacters = splitResult.Second; - if (currentLineBreak?.RemainingCharacters != null) + var lineBreak = remainingCharacters?.Count > 0 ? new TextLineBreak(remainingCharacters) : null; + + if (lineBreak is null && currentLineBreak.TextEndOfLine != null) { - if (remainingCharacters != null) - { - remainingCharacters.AddRange(currentLineBreak.RemainingCharacters); - } - else - { - remainingCharacters = new List(currentLineBreak.RemainingCharacters); - } + lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine); } - var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ? - new TextLineBreak(remainingCharacters) : - null; - - return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, - lineBreak); + return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, lineBreak); } /// diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 9c2a1953f1..a19f97e74e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; @@ -203,6 +204,40 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(expectedNumberOfLines, numberOfLines); } } + + [Fact] + public void Should_Wrap_RightToLeft() + { + using (Start()) + { + const string text = + "قطاعات الصناعة على الشبكة العالمية انترنيت ويونيكود، حيث ستتم، على الصعيدين الدولي والمحلي على حد سواء"; + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var currentTextSourceIndex = 0; + + while (currentTextSourceIndex < text.Length) + { + var textLine = + formatter.FormatLine(textSource, currentTextSourceIndex, 50, + new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap)); + + var glyphClusters = textLine.TextRuns.Cast() + .SelectMany(x => x.GlyphRun.GlyphClusters).ToArray(); + + Assert.True(glyphClusters[0] >= glyphClusters[^1]); + + Assert.Equal(currentTextSourceIndex, glyphClusters[^1]); + + currentTextSourceIndex += textLine.TextRange.Length; + } + } + } [InlineData("Whether to turn off HTTPS. This option only applies if Individual, " + "IndividualB2C, SingleOrg, or MultiOrg aren't used for ‑‑auth." From 43083edd2f9380c39ed9d2089c57e9286c67e795 Mon Sep 17 00:00:00 2001 From: Royce551 Date: Sat, 10 Jul 2021 10:57:28 -0500 Subject: [PATCH 153/184] Unsubscribe before stopping timer --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 1207b7f2dc..2fd08ef77c 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1024,8 +1024,8 @@ namespace Avalonia.Controls.Primitives return; } - _textSearchTimer.Stop(); _textSearchTimer.Tick -= TextSearchTimer_Tick; + _textSearchTimer.Stop(); _textSearchTimer = null; } From 17d9755c3f22f3a20b09996fc7d72e2ffd3f406e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 10 Jul 2021 21:43:19 +0200 Subject: [PATCH 154/184] Update API compat. --- src/Avalonia.Visuals/ApiCompatBaseline.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index c917902dc3..39a4c3004c 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -5,6 +5,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Task MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract. MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. +TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. @@ -73,4 +74,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWr InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. -Total Issues: 74 +Total Issues: 75 From e55c11ec37a1fb87fe7e376c86919f69936700ea Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 10 Jul 2021 21:57:53 +0200 Subject: [PATCH 155/184] Fix unit tests. --- .../StaticResourceExtensionTests.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs index 0ee7384c7d..fb3fd6d7d4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs @@ -29,7 +29,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); var border = userControl.FindControl("border"); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -72,7 +72,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); var border = userControl.FindControl("border"); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -119,7 +119,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); var border = userControl.FindControl("border"); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -149,7 +149,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); var border = userControl.FindControl("border"); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -200,7 +200,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var button = window.FindControl