From 2956f79c5c8268dd4d3f763324a501336e756394 Mon Sep 17 00:00:00 2001 From: Scott Whitney Date: Thu, 27 May 2021 21:52:37 +0930 Subject: [PATCH 01/96] 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 02/96] 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 03/96] 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 7ffc527cd5f76237ae92d32d2a81be6153853759 Mon Sep 17 00:00:00 2001 From: Scott Whitney Date: Sat, 29 May 2021 07:56:11 +0930 Subject: [PATCH 04/96] 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 a6cfe88c61bf971ce20fc6c9dabde5b3553a049e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Jun 2021 23:40:40 +0200 Subject: [PATCH 05/96] 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 06/96] 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 07/96] 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 4523450e0447928a11557618154df550b0c9051a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 12 Jun 2021 15:16:25 -0400 Subject: [PATCH 08/96] 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 11/96] 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 24/96] 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 25/96] 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 26/96] 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 cd51d638e93bad0eeb40408509a2e074ab16b56e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Jul 2021 08:51:29 +0200 Subject: [PATCH 28/96] Pass dialog flag to window impl Show. Not doing anything with it yet. --- .../Platform/IWindowBaseImpl.cs | 4 +++- src/Avalonia.Controls/Window.cs | 4 ++-- src/Avalonia.Controls/WindowBase.cs | 2 +- .../Remote/PreviewerWindowImpl.cs | 6 +----- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Headless/HeadlessWindowImpl.cs | 7 +------ src/Avalonia.Native/PopupImpl.cs | 4 ++-- src/Avalonia.Native/WindowImplBase.cs | 2 +- src/Avalonia.X11/X11Window.cs | 2 +- src/Windows/Avalonia.Win32/PopupImpl.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++-- .../ContextMenuTests.cs | 16 ++++++++-------- .../WindowBaseTests.cs | 2 +- .../Avalonia.UnitTests/MockWindowingPlatform.cs | 2 +- 15 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index 0d303a6666..5172569726 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -7,7 +7,9 @@ namespace Avalonia.Platform /// /// Shows the window. /// - void Show(bool activate); + /// Whether to activate the shown window. + /// Whether the window is being shown as a dialog. + void Show(bool activate, bool isDialog); /// /// Hides the window. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 700c3d9bad..f5a86b8b21 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -671,7 +671,7 @@ namespace Avalonia.Controls SetWindowStartupLocation(Owner?.PlatformImpl); - PlatformImpl?.Show(ShowActivated); + PlatformImpl?.Show(ShowActivated, false); Renderer?.Start(); } OnOpened(EventArgs.Empty); @@ -743,7 +743,7 @@ namespace Avalonia.Controls SetWindowStartupLocation(owner.PlatformImpl); - PlatformImpl?.Show(ShowActivated); + PlatformImpl?.Show(ShowActivated, true); Renderer?.Start(); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index cdcb499e98..3080def895 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -162,7 +162,7 @@ namespace Avalonia.Controls LayoutManager.ExecuteInitialLayoutPass(); _hasExecutedInitialLayoutPass = true; } - PlatformImpl?.Show(true); + PlatformImpl?.Show(true, false); Renderer?.Start(); OnOpened(EventArgs.Empty); } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 787f44887f..e82ec0fb0f 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -20,7 +20,7 @@ namespace Avalonia.DesignerSupport.Remote ClientSize = new Size(1, 1); } - public void Show(bool activate) + public void Show(bool activate, bool isDialog) { } @@ -99,10 +99,6 @@ namespace Avalonia.DesignerSupport.Remote { } - public void ShowDialog(IWindowImpl parent) - { - } - public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index eedfc52d9d..e5fd5a91e6 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -77,7 +77,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void Show(bool activate) + public void Show(bool activate, bool isDialog) { } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index af522f3e36..3c0dc5f6e2 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -75,7 +75,7 @@ namespace Avalonia.Headless public Action Closed { get; set; } public IMouseDevice MouseDevice { get; } - public void Show(bool activate) + public void Show(bool activate, bool isDialog) { if (activate) Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); @@ -147,11 +147,6 @@ namespace Avalonia.Headless } - public void ShowDialog(IWindowImpl parent) - { - Show(true); - } - public void SetSystemDecorations(bool enabled) { diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index c36675afcd..8740dd6f12 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -60,14 +60,14 @@ namespace Avalonia.Native } } - public override void Show(bool activate) + public override void Show(bool activate, bool isDialog) { var parent = _parent; while (parent is PopupImpl p) parent = p._parent; if (parent is WindowImpl w) w.Native.TakeFocusFromChildren(); - base.Show(false); + base.Show(false, isDialog); } public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, _glFeature, this); diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index f716464d14..f444db5bc1 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -351,7 +351,7 @@ namespace Avalonia.Native } - public virtual void Show(bool activate) + public virtual void Show(bool activate, bool isDialog) { _native.Show(activate.AsComBool()); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 37260aa78b..26edf10671 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -808,7 +808,7 @@ namespace Avalonia.X11 XSetTransientForHint(_x11.Display, _handle, parent.Handle.Handle); } - public void Show(bool activate) + public void Show(bool activate, bool isDialog) { _wasMappedAtLeastOnce = true; XMapWindow(_x11.Display, _handle); diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index dd3fd1342c..7b2d20a986 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.Win32 [ThreadStatic] private static IntPtr s_parentHandle; - public override void Show(bool activate) + public override void Show(bool activate, bool isDialog) { // Popups are always shown non-activated. UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index d92bd08d01..e3b01e8071 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -246,7 +246,7 @@ namespace Avalonia.Win32 public IWindowImpl CreateEmbeddableWindow() { var embedded = new EmbeddedWindowImpl(); - embedded.Show(true); + embedded.Show(true, false); return embedded; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 3a3342fd14..c46cbb3274 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -570,7 +570,7 @@ namespace Avalonia.Win32 _shown = false; } - public virtual void Show(bool activate) + public virtual void Show(bool activate, bool isDialog) { SetParent(_parent); ShowWindow(_showWindowState, activate); @@ -1120,7 +1120,7 @@ namespace Avalonia.Win32 SetParent(null); if (shown) - Show(activated); + Show(activated, false); } } else diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index f3a1316c7d..3d0c3b4c84 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -128,7 +128,7 @@ namespace Avalonia.Controls.UnitTests { using (Application()) { - popupImpl.Setup(x => x.Show(true)).Verifiable(); + popupImpl.Setup(x => x.Show(true, false)).Verifiable(); popupImpl.Setup(x => x.Hide()).Verifiable(); var sut = new ContextMenu(); @@ -148,7 +148,7 @@ namespace Avalonia.Controls.UnitTests _mouse.Click(target); Assert.False(sut.IsOpen); - popupImpl.Verify(x => x.Show(true), Times.Once); + popupImpl.Verify(x => x.Show(true, false), Times.Once); popupImpl.Verify(x => x.Hide(), Times.Once); } } @@ -158,7 +158,7 @@ namespace Avalonia.Controls.UnitTests { using (Application()) { - popupImpl.Setup(x => x.Show(true)).Verifiable(); + popupImpl.Setup(x => x.Show(true, false)).Verifiable(); popupImpl.Setup(x => x.Hide()).Verifiable(); var sut = new ContextMenu(); @@ -179,7 +179,7 @@ namespace Avalonia.Controls.UnitTests Assert.True(sut.IsOpen); popupImpl.Verify(x => x.Hide(), Times.Once); - popupImpl.Verify(x => x.Show(true), Times.Exactly(2)); + popupImpl.Verify(x => x.Show(true, false), Times.Exactly(2)); } } @@ -226,7 +226,7 @@ namespace Avalonia.Controls.UnitTests { using (Application()) { - popupImpl.Setup(x => x.Show(true)).Verifiable(); + popupImpl.Setup(x => x.Show(true, false)).Verifiable(); bool eventCalled = false; var sut = new ContextMenu(); @@ -242,7 +242,7 @@ namespace Avalonia.Controls.UnitTests Assert.True(eventCalled); Assert.False(sut.IsOpen); - popupImpl.Verify(x => x.Show(true), Times.Never); + popupImpl.Verify(x => x.Show(true, false), Times.Never); } } @@ -346,7 +346,7 @@ namespace Avalonia.Controls.UnitTests { using (Application()) { - popupImpl.Setup(x => x.Show(true)).Verifiable(); + popupImpl.Setup(x => x.Show(true, false)).Verifiable(); popupImpl.Setup(x => x.Hide()).Verifiable(); bool eventCalled = false; @@ -370,7 +370,7 @@ namespace Avalonia.Controls.UnitTests Assert.True(eventCalled); Assert.True(sut.IsOpen); - popupImpl.Verify(x => x.Show(true), Times.Once()); + popupImpl.Verify(x => x.Show(true, false), Times.Once()); popupImpl.Verify(x => x.Hide(), Times.Never); } } diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 8109b037c5..1b4214e0c7 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -137,7 +137,7 @@ namespace Avalonia.Controls.UnitTests var target = new TestWindowBase(windowImpl.Object); target.IsVisible = true; - windowImpl.Verify(x => x.Show(true)); + windowImpl.Verify(x => x.Show(true, false)); } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 8a24a8366f..713283aa2b 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -58,7 +58,7 @@ namespace Avalonia.UnitTests windowImpl.Object.Resized?.Invoke(clientSize); }); - windowImpl.Setup(x => x.Show(true)).Callback(() => + windowImpl.Setup(x => x.Show(true, It.IsAny())).Callback(() => { windowImpl.Object.Activated?.Invoke(); }); From 32c0eac41529158153ce0bc1294f73e5010ada4c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Jul 2021 09:27:46 +0200 Subject: [PATCH 29/96] Prevent dialog parent from becoming key window. --- native/Avalonia.Native/src/OSX/window.h | 1 + native/Avalonia.Native/src/OSX/window.mm | 42 +++++++++++++++++++++--- src/Avalonia.Native/WindowImplBase.cs | 2 +- src/Avalonia.Native/avn.idl | 2 +- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index b1f64bca88..e2f69c8359 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -34,6 +34,7 @@ class WindowBaseImpl; -(double) getScaling; -(double) getExtendedTitleBarHeight; -(void) setIsExtended:(bool)value; +-(bool) isDialog; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index d206d63a89..4d8303a173 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -105,7 +105,7 @@ public: return Window; } - virtual HRESULT Show(bool activate) override + virtual HRESULT Show(bool activate, bool isDialog) override { @autoreleasepool { @@ -488,6 +488,11 @@ public: return S_OK; } + virtual bool IsDialog() + { + return false; + } + protected: virtual NSWindowStyleMask GetStyle() { @@ -518,6 +523,7 @@ private: NSRect _preZoomSize; bool _transitioningWindowState; bool _isClientAreaExtended; + bool _isDialog; AvnExtendClientAreaChromeHints _extendClientHints; FORWARD_IUNKNOWN() @@ -573,11 +579,12 @@ private: } } - virtual HRESULT Show (bool activate) override + virtual HRESULT Show (bool activate, bool isDialog) override { @autoreleasepool - { - WindowBaseImpl::Show(activate); + { + _isDialog = isDialog; + WindowBaseImpl::Show(activate, isDialog); HideOrShowTrafficLights(); @@ -1070,6 +1077,11 @@ private: } } + virtual bool IsDialog() override + { + return _isDialog; + } + protected: virtual NSWindowStyleMask GetStyle() override { @@ -1858,6 +1870,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _isExtended = value; } +-(bool) isDialog +{ + return _parent->IsDialog(); +} + -(double) getScaling { return _lastScaling; @@ -2047,7 +2064,22 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent -(BOOL)canBecomeKeyWindow { - return _canBecomeKeyAndMain; + if (_canBecomeKeyAndMain) + { + // If the window has a child window being shown as a dialog then don't allow it to become the key window. + for(NSWindow* uch in [self childWindows]) + { + auto ch = objc_cast(uch); + if(ch == nil) + continue; + if (ch.isDialog) + return false; + } + + return true; + } + + return false; } -(BOOL)canBecomeMainWindow diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index f444db5bc1..0dba11af5a 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -353,7 +353,7 @@ namespace Avalonia.Native public virtual void Show(bool activate, bool isDialog) { - _native.Show(activate.AsComBool()); + _native.Show(activate.AsComBool(), isDialog.AsComBool()); } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index adcbeb2d3a..697b66f21a 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -430,7 +430,7 @@ interface IAvnString : IUnknown [uuid(e5aca675-02b7-4129-aa79-d6e417210bda)] interface IAvnWindowBase : IUnknown { - HRESULT Show(bool activate); + HRESULT Show(bool activate, bool isDialog); HRESULT Hide(); HRESULT Close(); HRESULT Activate(); From f0e72f73f62e234c9afc79034f01420ab3a44e73 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Jul 2021 10:50:47 +0200 Subject: [PATCH 30/96] Update ApiCompatBaseline.txt --- src/Avalonia.Controls/ApiCompatBaseline.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 46c12ebd39..1b2fbc5144 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -12,4 +12,7 @@ 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: 13 +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean, System.Boolean)' is present in the implementation but not in the contract. +Total Issues: 16 From e8da78cd41fffb23453567dc3c391f78101a095e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 5 Jul 2021 14:18:12 -0400 Subject: [PATCH 31/96] 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 cee1a24e0330d4daedea663e858ee33d761d2227 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 7 Jul 2021 17:06:59 +0200 Subject: [PATCH 32/96] 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 33/96] 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 34/96] 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 35/96] 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 8d0a4ff5ea5ca84304891a528f481f0c410bc2a1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 8 Jul 2021 02:31:39 -0400 Subject: [PATCH 36/96] 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 ac455785755a54811966dc565f3de95878cf7a12 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Jul 2021 11:37:28 +0100 Subject: [PATCH 37/96] 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 38/96] 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 39/96] 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 40/96] 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 41/96] 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 42/96] 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 43/96] 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 39394bceb6988571ceacd7f4fb85e142d41b5965 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 9 Jul 2021 09:29:58 +0200 Subject: [PATCH 44/96] 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 96ae9b19b30aa5fe3afe9467fd57acdd5e8e997b Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 9 Jul 2021 16:23:53 +0200 Subject: [PATCH 45/96] 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 46/96] 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 47/96] 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 48/96] 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 49/96] 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 50/96] 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 51/96] 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