From 5ea8d3d83afb3cd5819e501da682f209043be81c Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 30 Oct 2022 19:57:07 -0400 Subject: [PATCH 1/7] Add SizeChanged event --- src/Avalonia.Controls/Control.cs | 34 ++++++++++- src/Avalonia.Controls/SizeChangedEventArgs.cs | 60 +++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls/SizeChangedEventArgs.cs diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 524362fcf9d..d0bf24947b4 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -84,6 +84,13 @@ public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, I nameof(Unloaded), RoutingStrategies.Direct); + /// + /// Defines the event. + /// + public static readonly RoutedEvent SizeChangedEvent = + RoutedEvent.Register( + nameof(SizeChanged), RoutingStrategies.Bubble); + /// /// Defines the property. /// @@ -211,6 +218,15 @@ public event EventHandler? Unloaded remove => RemoveHandler(UnloadedEvent, value); } + /// + /// Occurs when the bounds (actual size) of the control have changed. + /// + public event EventHandler? SizeChanged + { + add => AddHandler(SizeChangedEvent, value); + remove => RemoveHandler(SizeChangedEvent, value); + } + public new IControl? Parent => (IControl?)base.Parent; /// @@ -530,14 +546,28 @@ protected override void OnKeyUp(KeyEventArgs e) } } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); - if (change.Property == FlowDirectionProperty) + if (change.Property == BoundsProperty) + { + var oldValue = change.GetOldValue(); + var newValue = change.GetNewValue(); + + var sizeChangedEventArgs = new SizeChangedEventArgs( + SizeChangedEvent, + source: this, + newSize: new Size(newValue.Width, newValue.Height), + previousSize: new Size(oldValue.Width, oldValue.Height)); + + RaiseEvent(sizeChangedEventArgs); + } + else if (change.Property == FlowDirectionProperty) { InvalidateMirrorTransform(); - + foreach (var visual in VisualChildren) { if (visual is Control child) diff --git a/src/Avalonia.Controls/SizeChangedEventArgs.cs b/src/Avalonia.Controls/SizeChangedEventArgs.cs new file mode 100644 index 00000000000..d983e375c22 --- /dev/null +++ b/src/Avalonia.Controls/SizeChangedEventArgs.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Provides data specific to a SizeChanged event. + /// + public class SizeChangedEventArgs : RoutedEventArgs + { + public SizeChangedEventArgs(RoutedEvent? routedEvent) + : base (routedEvent) + { + } + + public SizeChangedEventArgs(RoutedEvent? routedEvent, IInteractive? source) + : base(routedEvent, source) + { + } + + public SizeChangedEventArgs( + RoutedEvent? routedEvent, + IInteractive? source, + Size newSize, + Size previousSize) + : base(routedEvent, source) + { + NewSize = newSize; + PreviousSize = previousSize; + HeightChanged = newSize.Height != previousSize.Height; + WidthChanged = newSize.Width != previousSize.Width; + } + + /// + /// Gets a value indicating whether the height of the new size is different + /// than the previous size height. + /// + public bool HeightChanged { get; init; } + + /// + /// Gets the new size (or bounds) of the object. + /// + public Size NewSize { get; init; } + + /// + /// Gets the previous size (or bounds) of the object. + /// + public Size PreviousSize { get; init; } + + /// + /// Gets a value indicating whether the width of the new size is different + /// than the previous size width. + /// + public bool WidthChanged { get; init; } + } +} From 29e16e90da0a1cfeb88114b87f9fb1ec21fb0ad2 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 30 Oct 2022 20:18:09 -0400 Subject: [PATCH 2/7] Add more event member docs --- .../Interactivity/RoutedEventArgs.cs | 30 +++++++++++++++++++ src/Avalonia.Controls/Control.cs | 4 +-- src/Avalonia.Controls/SizeChangedEventArgs.cs | 29 ++++++++++++------ 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs b/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs index ccbb41b7dc5..60a6b646771 100644 --- a/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs +++ b/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs @@ -2,29 +2,59 @@ namespace Avalonia.Interactivity { + /// + /// Provices state information and data specific to a routed event. + /// public class RoutedEventArgs : EventArgs { + /// + /// Initializes a new instance of the class. + /// public RoutedEventArgs() { } + /// + /// Initializes a new instance of the class. + /// + /// The routed event associated with these event args. public RoutedEventArgs(RoutedEvent? routedEvent) { RoutedEvent = routedEvent; } + /// + /// Initializes a new instance of the class. + /// + /// The routed event associated with these event args. + /// The source object that raised the routed event. public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source) { RoutedEvent = routedEvent; Source = source; } + /// + /// Gets or sets a value indicating whether the routed event has already been handled. + /// + /// + /// Once handled, a routed event should be ignored. + /// public bool Handled { get; set; } + /// + /// Gets or sets the routed event associated with these event args. + /// public RoutedEvent? RoutedEvent { get; set; } + /// + /// Gets or sets the routing strategy (direct, bubbling, or tunneling) of the routed event. + /// public RoutingStrategies Route { get; set; } + /// + /// Gets or sets the source object that raised the routed event. + /// public IInteractive? Source { get; set; } } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index d0bf24947b4..827bded115a 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -559,8 +559,8 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang var sizeChangedEventArgs = new SizeChangedEventArgs( SizeChangedEvent, source: this, - newSize: new Size(newValue.Width, newValue.Height), - previousSize: new Size(oldValue.Width, oldValue.Height)); + previousSize: new Size(oldValue.Width, oldValue.Height), + newSize: new Size(newValue.Width, newValue.Height)); RaiseEvent(sizeChangedEventArgs); } diff --git a/src/Avalonia.Controls/SizeChangedEventArgs.cs b/src/Avalonia.Controls/SizeChangedEventArgs.cs index d983e375c22..201a00a3fc6 100644 --- a/src/Avalonia.Controls/SizeChangedEventArgs.cs +++ b/src/Avalonia.Controls/SizeChangedEventArgs.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Interactivity; +using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -12,25 +7,41 @@ namespace Avalonia.Controls /// public class SizeChangedEventArgs : RoutedEventArgs { + /// + /// Initializes a new instance of the class. + /// + /// The routed event associated with these event args. public SizeChangedEventArgs(RoutedEvent? routedEvent) : base (routedEvent) { } + /// + /// Initializes a new instance of the class. + /// + /// The routed event associated with these event args. + /// The source object that raised the routed event. public SizeChangedEventArgs(RoutedEvent? routedEvent, IInteractive? source) : base(routedEvent, source) { } + /// + /// Initializes a new instance of the class. + /// + /// The routed event associated with these event args. + /// The source object that raised the routed event. + /// The previous size (or bounds) of the object. + /// The new size (or bounds) of the object. public SizeChangedEventArgs( RoutedEvent? routedEvent, IInteractive? source, - Size newSize, - Size previousSize) + Size previousSize, + Size newSize) : base(routedEvent, source) { - NewSize = newSize; PreviousSize = previousSize; + NewSize = newSize; HeightChanged = newSize.Height != previousSize.Height; WidthChanged = newSize.Width != previousSize.Width; } From 66df8a51feaad72de04cd0a12f6efaacb5807f5e Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 30 Oct 2022 21:50:29 -0400 Subject: [PATCH 3/7] Change SizeChangedEvent routing strategy to direct This matches WPF and making it bubble was a mistake. --- src/Avalonia.Controls/Control.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 827bded115a..e21f0fd33d3 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -89,7 +89,7 @@ public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, I /// public static readonly RoutedEvent SizeChangedEvent = RoutedEvent.Register( - nameof(SizeChanged), RoutingStrategies.Bubble); + nameof(SizeChanged), RoutingStrategies.Direct); /// /// Defines the property. From 62485f53bc26d5f80795799b36df80b445c60d9f Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 30 Oct 2022 22:10:58 -0400 Subject: [PATCH 4/7] Only raise SizeChanged when the Size component changes (ignore position) --- src/Avalonia.Controls/Control.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index e21f0fd33d3..beaee34b076 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -556,13 +556,20 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang var oldValue = change.GetOldValue(); var newValue = change.GetNewValue(); - var sizeChangedEventArgs = new SizeChangedEventArgs( - SizeChangedEvent, - source: this, - previousSize: new Size(oldValue.Width, oldValue.Height), - newSize: new Size(newValue.Width, newValue.Height)); + // Bounds is a Rect with an X/Y Position as well as Height/Width. + // This means it is possible for the Rect to change position but not size. + // Therefore, we want to explicity check only the size and raise an event + // only when that size has changed. + if (newValue.Size != oldValue.Size) + { + var sizeChangedEventArgs = new SizeChangedEventArgs( + SizeChangedEvent, + source: this, + previousSize: new Size(oldValue.Width, oldValue.Height), + newSize: new Size(newValue.Width, newValue.Height)); - RaiseEvent(sizeChangedEventArgs); + RaiseEvent(sizeChangedEventArgs); + } } else if (change.Property == FlowDirectionProperty) { From bdd637298e7822aa3166c9097c99fd9fd49a91ef Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 31 Oct 2022 20:08:50 -0400 Subject: [PATCH 5/7] Take into account LayoutEpsilon when calculating Height/WidthChanged --- .../Interactivity/RoutedEventArgs.cs | 2 +- src/Avalonia.Controls/SizeChangedEventArgs.cs | 29 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs b/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs index 60a6b646771..2b660e70809 100644 --- a/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs +++ b/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs @@ -3,7 +3,7 @@ namespace Avalonia.Interactivity { /// - /// Provices state information and data specific to a routed event. + /// Provides state information and data specific to a routed event. /// public class RoutedEventArgs : EventArgs { diff --git a/src/Avalonia.Controls/SizeChangedEventArgs.cs b/src/Avalonia.Controls/SizeChangedEventArgs.cs index 201a00a3fc6..b3e399ff555 100644 --- a/src/Avalonia.Controls/SizeChangedEventArgs.cs +++ b/src/Avalonia.Controls/SizeChangedEventArgs.cs @@ -1,4 +1,6 @@ using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -42,14 +44,23 @@ public SizeChangedEventArgs( { PreviousSize = previousSize; NewSize = newSize; - HeightChanged = newSize.Height != previousSize.Height; - WidthChanged = newSize.Width != previousSize.Width; + + // Only consider changed when the size difference is greater than LayoutEpsilon + // This compensates for any rounding or precision difference between layout cycles + HeightChanged = !MathUtilities.AreClose(newSize.Height, previousSize.Height, LayoutHelper.LayoutEpsilon); + WidthChanged = !MathUtilities.AreClose(newSize.Width, previousSize.Width, LayoutHelper.LayoutEpsilon); } /// - /// Gets a value indicating whether the height of the new size is different - /// than the previous size height. + /// Gets a value indicating whether the height of the new size is considered + /// different than the previous size height. /// + /// + /// This will take into account layout epsilon and will not be true if both + /// heights are considered equivalent for layout purposes. Remember there can + /// be small variations in the calculations between layout cycles due to + /// rounding and precision even when the size has not otherwise changed. + /// public bool HeightChanged { get; init; } /// @@ -63,9 +74,15 @@ public SizeChangedEventArgs( public Size PreviousSize { get; init; } /// - /// Gets a value indicating whether the width of the new size is different - /// than the previous size width. + /// Gets a value indicating whether the width of the new size is considered + /// different than the previous size width. /// + /// + /// This will take into account layout epsilon and will not be true if both + /// heights are considered equivalent for layout purposes. Remember there can + /// be small variations in the calculations between layout cycles due to + /// rounding and precision even when the size has not otherwise changed. + /// public bool WidthChanged { get; init; } } } From 3e53621daee8b07be3e295e5e12931bdec658dc4 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 31 Oct 2022 20:12:21 -0400 Subject: [PATCH 6/7] Fix typo --- src/Avalonia.Controls/SizeChangedEventArgs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SizeChangedEventArgs.cs b/src/Avalonia.Controls/SizeChangedEventArgs.cs index b3e399ff555..fd40c505058 100644 --- a/src/Avalonia.Controls/SizeChangedEventArgs.cs +++ b/src/Avalonia.Controls/SizeChangedEventArgs.cs @@ -79,7 +79,7 @@ public SizeChangedEventArgs( /// /// /// This will take into account layout epsilon and will not be true if both - /// heights are considered equivalent for layout purposes. Remember there can + /// widths are considered equivalent for layout purposes. Remember there can /// be small variations in the calculations between layout cycles due to /// rounding and precision even when the size has not otherwise changed. /// From 66595bad2e9b532a434b6470eb67c2f32f718527 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 1 Nov 2022 07:45:03 -0400 Subject: [PATCH 7/7] Calculate Height/WidthChanged in the getter directly --- src/Avalonia.Controls/SizeChangedEventArgs.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/SizeChangedEventArgs.cs b/src/Avalonia.Controls/SizeChangedEventArgs.cs index fd40c505058..2dc642b1638 100644 --- a/src/Avalonia.Controls/SizeChangedEventArgs.cs +++ b/src/Avalonia.Controls/SizeChangedEventArgs.cs @@ -44,11 +44,6 @@ public SizeChangedEventArgs( { PreviousSize = previousSize; NewSize = newSize; - - // Only consider changed when the size difference is greater than LayoutEpsilon - // This compensates for any rounding or precision difference between layout cycles - HeightChanged = !MathUtilities.AreClose(newSize.Height, previousSize.Height, LayoutHelper.LayoutEpsilon); - WidthChanged = !MathUtilities.AreClose(newSize.Width, previousSize.Width, LayoutHelper.LayoutEpsilon); } /// @@ -61,7 +56,7 @@ public SizeChangedEventArgs( /// be small variations in the calculations between layout cycles due to /// rounding and precision even when the size has not otherwise changed. /// - public bool HeightChanged { get; init; } + public bool HeightChanged => !MathUtilities.AreClose(NewSize.Height, PreviousSize.Height, LayoutHelper.LayoutEpsilon); /// /// Gets the new size (or bounds) of the object. @@ -83,6 +78,6 @@ public SizeChangedEventArgs( /// be small variations in the calculations between layout cycles due to /// rounding and precision even when the size has not otherwise changed. /// - public bool WidthChanged { get; init; } + public bool WidthChanged => !MathUtilities.AreClose(NewSize.Width, PreviousSize.Width, LayoutHelper.LayoutEpsilon); } }